diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index f1349bf..799c82c 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -1,16 +1,27 @@ -# AGENTS.md - AI Agent Skills +# AGENTS.md - AI Agent Skills & Rules -This directory contains AI agent skills for this project. +This directory contains AI agent skills and project rules for this project. ## Structure ``` .agents/ -└── skills/ # Skill definitions (SKILL.md files) +├── rules/ # Project conventions and rules +│ └── icons.md # Icon componentization rules +└── skills/ # Skill definitions (SKILL.md files) └── my-skill/ └── SKILL.md ``` +## Rules + +Rules in `.agents/rules/` are project-level conventions that agents must follow. +They are not skills — they are enforced standards. + +| Rule | Applies to | Summary | +| -------------------------- | ---------- | ------------------------------------------------- | +| [icons.md](rules/icons.md) | All agents | Never write inline SVGs; use astro-icon or LcIcon | + ## Usage Use the `skills` CLI to manage skills: diff --git a/.agents/rules/icons.md b/.agents/rules/icons.md new file mode 100644 index 0000000..79b0f0e --- /dev/null +++ b/.agents/rules/icons.md @@ -0,0 +1,177 @@ +# Rules: Icon Componentization + +> **Status:** Enforced | **Applies to:** All agents writing components +> **Last updated:** 2026-04-30 + +--- + +## Rule 1: Never inline raw SVGs + +Do **not** write `` elements directly in `.astro`, `.tsx`, or `.ts` files. +All icons must be rendered through the project's icon systems (see Rule 2 and 3). + +**Violation:** + +```astro + + + + +``` + +**Correction:** + +```astro + + +``` + +**Exception:** The `` component (`src/components/donations/lucide-icon.tsx`) +uses a _single_ centralized `` wrapper. This is the **only** place raw `` +elements are allowed for icon rendering. + +--- + +## Rule 2: Astro components → use `astro-icon` + +In any `.astro` file, import and use the `Icon` component from `astro-icon`: + +```astro +--- +import { Icon } from 'astro-icon/components'; +--- + + + +``` + +### Available icon sets + +| Prefix | Package | Example usage | +| --------------- | ---------------------------- | --------------------- | +| `lucide:` | `@iconify-json/lucide` | `lucide:check` | +| `simple-icons:` | `@iconify-json/simple-icons` | `simple-icons:github` | + +Both packages are listed in `package.json`. Do **not** install additional +iconify packages without explicit approval. + +### Styling + +- Use **Tailwind classes** (`class="w-5 h-5 text-primary"`), not inline `width`/`height` props. +- Override `stroke-width` via the prop: ``. +- Always include `aria-hidden="true"` for decorative icons (the default). + +--- + +## Rule 3: SolidJS components → use `` + +In `.tsx` files using SolidJS (e.g., `DonationWidget.tsx`), import from +`src/components/donations/icons.tsx` — **never** import `LcIcon` directly: + +```tsx +// ✅ Correct — use named exports from icons.tsx +import { CheckIcon, GiftIcon, SparklesIcon } from "./icons"; + +// ❌ Wrong — bypasses the named exports +import { LcIcon } from "./lucide-icon"; +``` + +### Available named exports (`icons.tsx`) + +| Export | Underlying icon | Default size | +| -------------- | --------------------------- | -------------- | +| `CheckIcon` | `lucide:check` | 16×16 | +| `ClockIcon` | `lucide:clock` | 24×24 | +| `CoinIcon` | `lucide:circle-dollar-sign` | 18×18 | +| `CopyIcon` | `lucide:copy` | 16×16 | +| `ErrorIcon` | `lucide:circle-x` | 16×16 | +| `GiftIcon` | `lucide:gift` | 20×20 | +| `HeartIcon` | `lucide:heart` | 24×24 (filled) | +| `LockIcon` | `lucide:lock` | 14×14 | +| `SparklesIcon` | `lucide:sparkles` | 24×24 (filled) | + +These thin wrappers live in `src/components/donations/icons.tsx` and delegate to +`` from `src/components/donations/lucide-icon.tsx`. + +### Adding a new icon + +1. Open `src/components/donations/lucide-icon.tsx`. +2. Add the icon name to the `LcIconName` union type. +3. Extract its SVG `body` from `node_modules/@iconify-json/lucide/icons.json` + and add it to the `ICON_BODIES` lookup. +4. Create a named export in `src/components/donations/icons.tsx` following the + existing pattern. +5. Update the table above in this document. + +```tsx +// Step 1: add to type +export type LcIconName = "gift" | /* ... */ | "my-new-icon"; + +// Step 2: add body +const ICON_BODIES: Record = { + // ... + "my-new-icon": `...`, +}; + +// Step 4: add named export in icons.tsx +export function MyNewIcon(props: { class?: string }) { + return ; +} +``` + +--- + +## Rule 4: Unicode symbols → use icons + +Do not use raw Unicode symbols for UI decoration. Replace them with the +appropriate `astro-icon` ``: + +| Instead of | Use | +| ---------- | ---------------------------------------------------- | +| `✓` | `` | +| `✗` `✘` | `` | +| `→` `➔` | `` | +| `⬤` `●` | `` | +| `★` `☆` | `` | +| `♥` `❤` | `` | + +--- + +## Architecture + +``` + ┌─────────────────────┐ + │ Astro (.astro) │ + │ │ + │ │ ← astro-icon looks up + │ │ @iconify-json/lucide + └─────────────────────┘ or simple-icons at build time + + ┌─────────────────────┐ + │ SolidJS (.tsx) │ + │ │ + │ │ ← imports from ./icons + │ │ + │ │ │ + │ ▼ │ + │ │ ← SVG bodies extracted + │ │ from @iconify-json/lucide + │ lucide-icon.tsx │ at build time (tree-shaken) + └─────────────────────┘ +``` + +- **No runtime iconify library** is loaded on the client. +- **SolidJS**: Only the 10 icon bodies we use are bundled (~2 KB gzipped). +- **Astro**: Icons are inlined at build time by `astro-icon` with zero client cost. + +--- + +## Enforcement + +- **Linting:** Run `bun run lint` to catch raw `` violations if a lint rule + is added in the future. +- **Code review:** Any PR introducing inline `` elements outside + `lucide-icon.tsx` must be rejected. +- **TypeScript:** The `LcIconName` union type prevents typos in icon names + in SolidJS components. diff --git a/.agents/scripts/validate-dag.py b/.agents/scripts/validate-dag.py new file mode 100755 index 0000000..5f31b39 --- /dev/null +++ b/.agents/scripts/validate-dag.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Validate a tasks.json DAG for prd-to-tasks and implement-tasks skills. + +Checks: + 1. Valid JSON + 2. All dependency targets exist + 3. No circular dependencies (Kahn's algorithm) + 4. Unique task IDs + 5. Phase keys match referenced phases + 6. Every task has agent and moeExperts fields + 7. Top-level agents key matches task assignments + 8. Topological sort output + +Usage: + python3 validate-dag.py # standard validation + python3 validate-dag.py --quiet # exit code only + python3 validate-dag.py --summary # include stats + python3 validate-dag.py --order # print space-separated topological order only +""" + +import json +import sys +from collections import deque +from pathlib import Path + + +def fail(msg: str) -> None: + print(f"❌ {msg}", file=sys.stderr) + sys.exit(1) + + +def warn(msg: str) -> None: + print(f"⚠️ {msg}", file=sys.stderr) + + +def ok(msg: str) -> None: + print(f"✅ {msg}") + + +def validate(tasks_path: str, quiet: bool = False, summary: bool = False) -> dict: + """Validate a tasks.json file. Returns stats dict on success.""" + path = Path(tasks_path) + if not path.exists(): + fail(f"File not found: {tasks_path}") + + # ── 1. Valid JSON ────────────────────────────────────────────── + try: + data = json.loads(path.read_text()) + except json.JSONDecodeError as e: + fail(f"Invalid JSON: {e}") + if not quiet: + ok("Valid JSON") + + # ── 2. Required top-level keys ───────────────────────────────── + if "tasks" not in data: + fail("Missing required key: 'tasks'") + + tasks_list = data["tasks"] + if not isinstance(tasks_list, list): + fail("'tasks' must be an array") + + if not tasks_list: + warn("Task list is empty — nothing to validate") + return {"total_tasks": 0, "total_hours": 0, "order": []} + + tasks = {} + for t in tasks_list: + tid = t.get("id") + if not tid: + fail(f"Task missing 'id' field: {t.get('title', 'UNKNOWN')}") + if tid in tasks: + fail(f"Duplicate task ID: {tid}") + tasks[tid] = t + + task_ids = set(tasks.keys()) + + if not quiet: + ok(f"Parsed {len(tasks)} tasks") + + # ── 3. All dependency targets exist ──────────────────────────── + errors = 0 + for tid, t in tasks.items(): + for dep in t.get("dependencies", []): + if dep not in task_ids: + fail(f"{tid} depends on missing task '{dep}'") + errors += 1 + + if not quiet: + ok("All dependency targets exist" if not errors else f"{errors} missing deps") + + # ── 4. Detect cycles (Kahn's algorithm) + topological sort ───── + in_degree = {tid: 0 for tid in task_ids} + adj = {tid: [] for tid in task_ids} + for tid, t in tasks.items(): + for dep in t.get("dependencies", []): + adj[dep].append(tid) + in_degree[tid] += 1 + + q = deque([tid for tid, deg in in_degree.items() if deg == 0]) + order = [] + while q: + tid = q.popleft() + order.append(tid) + for neighbor in adj[tid]: + in_degree[neighbor] -= 1 + if in_degree[neighbor] == 0: + q.append(neighbor) + + if len(order) != len(task_ids): + remaining = task_ids - set(order) + fail(f"Circular dependency involving: {', '.join(sorted(remaining))}") + + if not quiet: + ok("No circular dependencies (valid DAG)") + if summary: + print(f" Topological order: {' → '.join(order)}") + + # ── 5. Phase keys match ──────────────────────────────────────── + if "phases" in data: + phase_ids = set(data["phases"].keys()) + for t in tasks_list: + phase = t.get("phase") + if phase and phase not in phase_ids: + warn(f"{t['id']} references unknown phase '{phase}'") + elif not quiet: + ok("No phases defined (skipping phase validation)") + + # ── 6. Every task has agent and moeExperts ───────────────────── + missing_agent = [t["id"] for t in tasks_list if not t.get("agent")] + missing_experts = [t["id"] for t in tasks_list if not t.get("moeExperts")] + if missing_agent: + fail(f"Tasks missing 'agent' field: {', '.join(missing_agent)}") + if missing_experts: + fail(f"Tasks missing 'moeExperts' field: {', '.join(missing_experts)}") + if not quiet: + ok("All tasks have agent and moeExperts fields") + + # ── 7. agents key matches task assignments ───────────────────── + if "agents" in data: + for agent_name, agent_data in data["agents"].items(): + listed = set(agent_data.get("tasks", [])) + actual = {t["id"] for t in tasks_list if t.get("agent") == agent_name} + if listed != actual: + warn(f"'{agent_name}' task list mismatch:") + if listed - actual: + warn(f" In agents but not assigned: {sorted(listed - actual)}") + if actual - listed: + warn(f" Assigned but not in agents: {sorted(actual - listed)}") + if not quiet: + ok("Agent summary matches task assignments") + elif not quiet: + ok("No agents key (skipping agent validation)") + + # ── 8. Summary stats ─────────────────────────────────────────── + total_hours = sum(t.get("estimatedHours", 0) for t in tasks_list) + if summary: + print(f"\n📊 Summary:") + print(f" Total tasks: {len(tasks)}") + print(f" Total estimated hours: {total_hours}") + print(f" Phases: {len(data.get('phases', {}))}") + print(f" Agents: {len(data.get('agents', {}))}") + + if "phases" in data: + for phase_id, phase_data in data["phases"].items(): + phase_tasks = phase_data.get("tasks", []) + phase_hours = sum( + tasks[tid]["estimatedHours"] + for tid in phase_tasks + if tid in tasks and "estimatedHours" in tasks[tid] + ) + print(f" {phase_data.get('label', phase_id)}: {len(phase_tasks)} tasks, {phase_hours}h") + + return { + "total_tasks": len(tasks), + "total_hours": total_hours, + "order": order, + } + + +def main() -> None: + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [--quiet] [--summary]", file=sys.stderr) + sys.exit(2) + + tasks_path = sys.argv[1] + quiet = "--quiet" in sys.argv + summary = "--summary" in sys.argv + order_only = "--order" in sys.argv + + if order_only: + # Machine-readable: print topological order only (space-separated IDs) + path = Path(tasks_path) + data = json.loads(path.read_text()) + tasks_list = data.get("tasks", []) + tasks = {t["id"]: t for t in tasks_list} + task_ids = set(tasks.keys()) + + in_degree = {tid: 0 for tid in task_ids} + adj = {tid: [] for tid in task_ids} + for tid, t in tasks.items(): + for dep in t.get("dependencies", []): + adj[dep].append(tid) + in_degree[tid] += 1 + + q = deque([tid for tid, deg in in_degree.items() if deg == 0]) + order = [] + while q: + tid = q.popleft() + order.append(tid) + for neighbor in adj[tid]: + in_degree[neighbor] -= 1 + if in_degree[neighbor] == 0: + q.append(neighbor) + + if len(order) != len(task_ids): + remaining = task_ids - set(order) + fail(f"Circular dependency involving: {', '.join(sorted(remaining))}") + + print(" ".join(order)) + sys.exit(0) + + validate(tasks_path, quiet=quiet, summary=summary) + if not quiet: + print("\n🎉 Validation passed!") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/grill-me/SKILL.md b/.agents/skills/grill-me/SKILL.md index a8714ad..982b050 100644 --- a/.agents/skills/grill-me/SKILL.md +++ b/.agents/skills/grill-me/SKILL.md @@ -150,14 +150,13 @@ Which do you prefer? ## Anti-Patterns -| Don't | Do Instead | -| --------------------------------------- | ----------------------------------------------------------------------- | -| Ask 5 questions at once | Ask the most important one, let the answer guide the next | -| Ask questions answerable by `grep` | Search the codebase first | -| Ask "What do you want?" with no options | Present concrete options with trade-offs | -| Guess and hope it's right | Spend 30 seconds asking vs hours redoing | -| Keep asking when 80% clarity is enough | Accept reasonable defaults for low-impact details | -| Start implementing after clarification | Hand off to `create-prd` or `implement-tasks`. This skill never builds. | +| Don't | Do Instead | +| --------------------------------------- | --------------------------------------------------------- | +| Ask 5 questions at once | Ask the most important one, let the answer guide the next | +| Ask questions answerable by `grep` | Search the codebase first | +| Ask "What do you want?" with no options | Present concrete options with trade-offs | +| Guess and hope it's right | Spend 30 seconds asking vs hours redoing | +| Keep asking when 80% clarity is enough | Accept reasonable defaults for low-impact details | ## Examples diff --git a/.agents/skills/implement-tasks/SKILL.md b/.agents/skills/implement-tasks/SKILL.md index e88d2c1..767d690 100644 --- a/.agents/skills/implement-tasks/SKILL.md +++ b/.agents/skills/implement-tasks/SKILL.md @@ -8,6 +8,10 @@ description: > Do NOT use when no tasks.json exists (run prd-to-tasks first), for a single straightforward task (just do it directly), or when the user wants manual control over each step. +metadata: + scripts: + - ../../scripts/validate-dag.py + runtime: python3 --- # Implement Tasks @@ -95,58 +99,15 @@ Before implementing, check for common issues: 4. **All tasks have agent and moeExperts** — Required for delegation (added by `prd-to-tasks` step 6) 5. **Phase ordering is logical** — Foundation before features, core before polish -Run validation with: +Run validation with the shared DAG validator: ```bash -python3 -c " -import json, sys -from collections import deque - -data = json.load(open('tasks.json')) -tasks = {t['id']: t for t in data['tasks']} -task_ids = set(tasks.keys()) - -# Check all deps exist -for tid, t in tasks.items(): - for dep in t['dependencies']: - if dep not in task_ids: - print(f'ERROR: {tid} depends on missing task {dep}') - sys.exit(1) - -# Detect cycles via Kahn's algorithm -in_degree = {tid: 0 for tid in task_ids} -adj = {tid: [] for tid in task_ids} -for tid, t in tasks.items(): - for dep in t['dependencies']: - adj[dep].append(tid) - in_degree[tid] += 1 - -q = deque([tid for tid, deg in in_degree.items() if deg == 0]) -sorted_tasks = [] -while q: - tid = q.popleft() - sorted_tasks.append(tid) - for neighbor in adj[tid]: - in_degree[neighbor] -= 1 - if in_degree[neighbor] == 0: - q.append(neighbor) - -if len(sorted_tasks) != len(task_ids): - remaining = task_ids - set(sorted_tasks) - print(f'ERROR: Circular dependency involving tasks: {remaining}') - sys.exit(1) - -print('Task graph is valid (DAG)') -print(f'Topological order: {\" → \".join(sorted_tasks)}') - -# Critical path -# (simplified: longest path by estimatedHours) -# ... (expand as needed) -print(f'Total tasks: {len(tasks)}, Total estimated hours: {sum(t[\"estimatedHours\"] for t in tasks.values())}') -" +../../scripts/validate-dag.py tasks.json --summary ``` -**If validation fails**, stop and report the issues. Do NOT proceed until fixed. +This checks: valid JSON, missing dependencies, circular dependencies (Kahn's algorithm), phase keys, unique IDs, agent/moeExperts fields, and agent summary consistency. With `--summary` it also prints the topological order and hour estimates. + +**If validation fails**, the script exits with code 1 and prints a specific error. Stop and report the issues. Do NOT proceed until fixed. ### Step 3: Determine Execution Order @@ -358,29 +319,8 @@ echo "" TOTAL=$(python3 -c "import json; print(json.load(open('$TASKS_FILE'))['metadata']['totalTasks'])") echo "Total tasks: $TOTAL" -# Get topological order -ORDER=$(python3 -c " -import json -from collections import deque -data = json.load(open('$TASKS_FILE')) -tasks = {t['id']: t for t in data['tasks']} -in_degree = {tid: 0 for tid in tasks} -adj = {tid: [] for tid in tasks} -for tid, t in tasks.items(): - for dep in t['dependencies']: - adj[dep].append(tid) - in_degree[tid] += 1 -q = deque([tid for tid, deg in in_degree.items() if deg == 0]) -order = [] -while q: - tid = q.popleft() - order.append(tid) - for n in adj[tid]: - in_degree[n] -= 1 - if in_degree[n] == 0: - q.append(n) -print(' '.join(order)) -") +# Get topological order from the shared DAG validator +ORDER=$(../../scripts/validate-dag.py "$TASKS_FILE" --order) echo "Execution order: $ORDER" echo "" diff --git a/.agents/skills/prd-to-tasks/SKILL.md b/.agents/skills/prd-to-tasks/SKILL.md index 5045d3e..b2c52dd 100644 --- a/.agents/skills/prd-to-tasks/SKILL.md +++ b/.agents/skills/prd-to-tasks/SKILL.md @@ -6,6 +6,10 @@ description: > Use when a PRD exists and the user wants to break it down into implementable tasks, or says "turn this into tasks," "create issues from the PRD," or "what are the next steps?" Do NOT use when there is no PRD or when the user wants to skip planning and start coding immediately. +metadata: + scripts: + - ../../scripts/validate-dag.py + runtime: python3 --- # PRD to Tasks @@ -305,53 +309,20 @@ match exactly one agent entry. ### 7. Validate the File -Run these checks: +Run the shared DAG validator. It checks everything: valid JSON, missing dependencies, circular dependencies, phase keys, unique IDs, agent/moeExperts fields, and the agents summary. ```bash -# Parse JSON is valid -cat tasks.json | python3 -c "import json,sys; json.load(sys.stdin); print('Valid JSON')" - -# No missing dependency targets -cat tasks.json | python3 -c " -import json, sys -data = json.load(sys.stdin) -tasks = {t['id'] for t in data['tasks']} -for t in data['tasks']: - for dep in t['dependencies']: - if dep not in tasks: - print(f'ERROR: {t[\"id\"]} depends on missing task {dep}') -print('Dependencies OK') -" - -# No circular dependencies (topological sort must succeed) -# Check phase keys match -# Check IDs are unique - -# Every task has agent and moeExperts assigned -cat tasks.json | python3 -c " -import json -data = json.load(open('tasks.json')) -for t in data['tasks']: - if 'agent' not in t: - print(f'ERROR: {t[\"id\"]} missing agent field') - if 'moeExperts' not in t: - print(f'ERROR: {t[\"id\"]} missing moeExperts field') -print('Agent/MoE fields OK') -" - -# agents key matches task assignments -cat tasks.json | python3 -c " -import json -data = json.load(open('tasks.json')) -for agent_name, agent_data in data.get('agents', {}).items(): - listed = set(agent_data['tasks']) - actual = {t['id'] for t in data['tasks'] if t.get('agent') == agent_name} - if listed != actual: - print(f'WARN: {agent_name} task list mismatch') -print('Agent summary OK') -" +# Run from the skill directory; validates the tasks.json in cwd +../../scripts/validate-dag.py tasks.json --summary ``` +Flags: + +- `--summary` — include topological order, phase breakdown, and hour estimates +- `--quiet` — exit code only (useful in scripts) + +If validation fails, the script exits with code 1 and prints a specific error. Fix the issue before proceeding. + ### 8. Present for Review Summarize the task breakdown including agent assignments: diff --git a/AGENTS.md b/AGENTS.md index ae88015..3f24575 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -281,6 +281,7 @@ Required for deployment (Cloudflare/Pulumi credentials). | `astro.config.ts` | Site configuration, i18n settings | | `biome.json` | Code style and linting rules | | `.opencode/learnings/registry.md` | Captured learnings from past work | +| `.agents/rules/` | Project conventions (icons, styles) | --- diff --git a/astro.config.ts b/astro.config.ts index 82dc8a7..c87e5f7 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -2,6 +2,7 @@ import cloudflare from '@astrojs/cloudflare'; import mdx from '@astrojs/mdx'; import sitemap from '@astrojs/sitemap'; +import solidJs from '@astrojs/solid-js'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig, fontProviders } from 'astro/config'; import icon from 'astro-icon'; @@ -22,6 +23,7 @@ export default defineConfig({ integrations: [ mdx(), sitemap(), + solidJs(), icon({ include: { 'simple-icons': ['github', 'linkedin', 'instagram', 'youtube', 'x', 'discord'], diff --git a/bun.lock b/bun.lock index 29cf390..ae6229c 100644 --- a/bun.lock +++ b/bun.lock @@ -10,13 +10,16 @@ "@astrojs/mdx": "^5.0.3", "@astrojs/rss": "^4.0.18", "@astrojs/sitemap": "^3.7.2", + "@astrojs/solid-js": "^6.0.1", "@iconify-json/lucide": "^1.2.102", "@iconify-json/simple-icons": "^1.2.79", "@tailwindcss/vite": "^4.2.2", "astro": "^6.1.8", "astro-icon": "^1.1.5", "daisyui": "^5.5.19", + "qrcode": "^1.5.4", "sharp": "^0.34.5", + "solid-js": "^1.9.12", "tailwindcss": "^4.2.2", "typescript": "^6.0.3", "wrangler": "^4.83.0", @@ -24,6 +27,7 @@ "devDependencies": { "@biomejs/biome": "^2.4.12", "@playwright/test": "^1.59.1", + "@types/qrcode": "^1.5.6", "lefthook": "^2.1.6", "playwright": "^1.59.1", "vite-tsconfig-paths": "^6.1.1", @@ -73,18 +77,48 @@ "@astrojs/sitemap": ["@astrojs/sitemap@3.7.2", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA=="], + "@astrojs/solid-js": ["@astrojs/solid-js@6.0.1", "", { "dependencies": { "vite": "^7.3.1", "vite-plugin-solid": "^2.11.10" }, "peerDependencies": { "solid-devtools": "^0.30.1", "solid-js": "^1.8.5" }, "optionalPeers": ["solid-devtools"] }, "sha512-6g2DEtznW2ithiaDY3qyCSdnyNBjpXfKR2qCnvxxdmZZlO+8AC85KkEX4BpnJrzVfy7ptx0/WYKuBRCFdheo8Q=="], + "@astrojs/telemetry": ["@astrojs/telemetry@3.3.1", "", { "dependencies": { "ci-info": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^4.0.0", "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" } }, "sha512-7fcIxXS9J4ls5tr8b3ww9rbAIz2+HrhNJYZdkAhhB4za/I5IZ/60g+Bs8q7zwG0tOIZfNB4JWhVJ1Qkl/OrNCw=="], "@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.3", "", {}, "sha512-cxnGSw+sJigBLdX4TMSZKkzV6C3gMLJMucDk2W+n281Xhie68T2/9f1+1NMNDCZsc5i0FED7Qt5I10g2O9wtZg=="], "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.3", "", { "dependencies": { "yaml": "^2.8.2" } }, "sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@biomejs/biome": ["@biomejs/biome@2.4.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.12", "@biomejs/cli-darwin-x64": "2.4.12", "@biomejs/cli-linux-arm64": "2.4.12", "@biomejs/cli-linux-arm64-musl": "2.4.12", "@biomejs/cli-linux-x64": "2.4.12", "@biomejs/cli-linux-x64-musl": "2.4.12", "@biomejs/cli-win32-arm64": "2.4.12", "@biomejs/cli-win32-x64": "2.4.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA=="], @@ -507,6 +541,14 @@ "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -533,6 +575,8 @@ "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], @@ -615,10 +659,16 @@ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.24", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA=="], + "bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="], "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], @@ -627,6 +677,8 @@ "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -635,6 +687,10 @@ "cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -699,10 +755,14 @@ "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], @@ -719,6 +779,8 @@ "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -731,6 +793,8 @@ "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.345", "", {}, "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg=="], + "emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -809,6 +873,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], @@ -821,6 +887,8 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "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-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], @@ -867,6 +935,8 @@ "hosted-git-info": ["hosted-git-info@9.0.2", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg=="], + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], @@ -915,20 +985,28 @@ "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], @@ -989,6 +1067,8 @@ "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -1043,6 +1123,8 @@ "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "micromark": ["micromark@4.0.2", "", { "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-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], @@ -1161,6 +1243,8 @@ "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], "normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], @@ -1201,12 +1285,16 @@ "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], "p-queue": ["p-queue@9.1.2", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^7.0.0" } }, "sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw=="], "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "pacote": ["pacote@21.5.0", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-VtZ0SB8mb5Tzw3dXDfVAIjhyVKUHZkS/ZH9/5mpKenwC9sFOXNI0JI7kEF7IMkwOnsWMFrvAZHzx1T5fmrp9FQ=="], @@ -1225,6 +1313,8 @@ "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -1251,6 +1341,8 @@ "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], @@ -1273,6 +1365,8 @@ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], @@ -1325,6 +1419,8 @@ "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], @@ -1343,6 +1439,12 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], + + "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], + + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1371,6 +1473,10 @@ "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], + + "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1495,6 +1601,8 @@ "upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], + "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=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], @@ -1509,6 +1617,8 @@ "vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], + "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], @@ -1561,6 +1671,8 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], "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=="], @@ -1605,6 +1717,12 @@ "@astrojs/language-server/@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@iconify/tools/svgo": ["svgo@3.3.3", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0", "sax": "^1.5.0" }, "bin": "./bin/svgo" }, "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng=="], @@ -1633,6 +1751,8 @@ "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "cheerio/undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], @@ -1657,8 +1777,12 @@ "normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], + "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "spdx-correct/spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], @@ -1679,6 +1803,8 @@ "yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@iconify/tools/svgo/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], "@iconify/tools/svgo/css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], @@ -1701,6 +1827,12 @@ "normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + + "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], @@ -1756,5 +1888,7 @@ "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "@iconify/tools/svgo/css-tree/mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], + + "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], } } diff --git a/docs/PRD-pix-donations.md b/docs/PRD-pix-donations.md deleted file mode 100644 index bcdf1f9..0000000 --- a/docs/PRD-pix-donations.md +++ /dev/null @@ -1,313 +0,0 @@ -# PRD: PIX Donations - -**Status:** Draft -**Created:** 2026-04-28 -**Author:** AI Agent (from discussion with maintainer) -**Version:** 1.1 -**Issue:** [#248](https://github.com/podcodar/webapp/issues/248) - ---- - -## Executive Summary - -Add a self-service PIX donation section to the `/contributing` page. Visitors -choose a suggested amount (R$ 25, R$ 50, R$ 100) or enter a custom value -(R$ 5–R$ 10,000) and instantly receive a PIX QR code and "Copia e Cola" string -to complete their donation — no backend, no contact form required. - ---- - -## Problem Statement - -The current donations flow requires visitors to fill out a contact form -("Falar sobre doação" → `/contact`) before they can donate. This friction -loses potential donors who want to contribute immediately. Other Brazilian -community projects offer one-click PIX donations, and PodCodar should match -that expectation. - ---- - -## Goals - -- Let anyone donate via PIX in under 30 seconds without leaving the page -- Generate valid PIX BR Code + QR code entirely client-side (no server cost) -- Highlight the donation section prominently on the contributing page -- Validate custom amounts with clear error messages (Zod) -- Keep the existing contact-based flow for donors who want to discuss larger - contributions (> R$ 10,000) - ---- - -## Non-Goals - -- **No payment tracking or confirmation** — this is a static site; we cannot - know if a PIX payment was completed -- **No recurring donations** — one-time PIX only -- **No backend or server-side code** — all generation happens in the browser -- **No donor identity collection** — anonymous donations are fine -- **No dynamic PIX keys** — single key (`doar@podcodar.org`), no per-donor - or per-amount keys -- **No htmx** — current interactivity needs are fully covered by Alpine.js - ---- - -## User Stories - -### Must Have (P0) - -- As a visitor, I want to select a suggested donation amount (R$ 25, R$ 50, R$ 100) - with one click so that I can donate quickly without typing. -- As a visitor, I want to enter a custom donation amount so that I can donate - any value I choose. -- As a visitor, I want to see a PIX QR code for my chosen amount so that I can - scan it with my banking app. -- As a visitor, I want to copy the "Copia e Cola" PIX string to my clipboard - so that I can paste it into my banking app. -- As a visitor, I want clear validation errors if I enter an invalid amount - so that I can fix it and proceed. - -### Should Have (P1) - -- As a visitor, I want a visual distinction between the QR code view and the - "Copia e Cola" view so I can choose my preferred method. -- As a donor considering a large contribution (> R$ 10,000), I want to be - directed to the contact form instead of being blocked, so that I can discuss - the donation with the team. - ---- - -## Functional Requirements - -### FR-1: Donation Amount Selection - -- System must display three suggested amount buttons: R$ 25, R$ 50, R$ 100 -- System must provide a custom amount input field -- System must validate the custom input using Zod: - - Minimum: R$ 5.00 - - Maximum: R$ 10,000.00 - - Must be a valid positive number (two decimal places allowed) -- System must display validation errors inline in Portuguese -- Selecting a suggested button must populate the custom input and generate the - PIX code immediately -- Changing the custom input must regenerate the PIX code on valid input - -### FR-2: PIX BR Code Generation - -- System must generate a valid EMV QRCPS-MPM compliant PIX string client-side -- PIX key: `doar@podcodar.org` (email type) -- Required fields in the generated string: - - `00` — Payload Format Indicator: `01` - - `26` — Merchant Account Information: - - `00` — GUI: `br.gov.bcb.pix` - - `01` — PIX key: `doar@podcodar.org` - - `52` — Merchant Category Code: `0000` - - `53` — Transaction Currency: `986` (BRL) - - `54` — Transaction Amount: user-selected value (when fixed amount) - - `58` — Country Code: `BR` - - `59` — Merchant Name: `PodCodar` (max 25 chars) - - `60` — Merchant City: `Belo Horizonte` (max 15 chars) - - `63` — CRC16-CCITT checksum over the full string (excluding the `6304` trailer) -- System must compute the CRC16-CCITT checksum per the PIX standard -- When no amount is selected or input is invalid, no PIX code must be generated - -### FR-3: QR Code Display - -- System must render a QR code from the generated PIX string using **inline SVG** - (no external QR library — implement a minimal QR code SVG generator as a shared - utility in `src/lib/qrcode.ts`) -- QR code must be scannable by Brazilian banking apps (PIX-compatible) -- QR code dimensions: minimum 200×200px, should scale on larger screens -- QR code must update immediately when the amount changes - -### FR-4: Copia e Cola - -- System must display a **truncated** preview of the PIX string (e.g., first - 40 characters + "…") with a toggle to reveal the full string -- System must provide a "Copiar" button that copies the full PIX string to the - clipboard -- System must show visual feedback on successful copy (e.g., button text changes - to "Copiado!" for 2 seconds) -- System must handle clipboard API unavailability gracefully (fallback: - select text for manual copy) - -### FR-5: Large Donations (> R$ 10,000) - -- When the user enters an amount > R$ 10,000, system must: - - Not generate a PIX code - - Show a message directing the donor to contact the team - - Provide a link to `/contact` -- The validation error for "amount too high" must differ from "invalid format" - errors — it should be informative, not an error state - -### FR-6: Section Layout - -- The PIX donation section must be placed immediately after the hero on - `/contributing` -- The section must be visually highlighted / prominent (distinct background or - border treatment) -- The section must replace the current "Falar sobre doação" CTA that links - to `/contact` -- Responsive layout: buttons in a row on desktop, stack on mobile - ---- - -## Non-Functional Requirements - -| Category | Requirement | Target | -| ------------- | ----------------------------- | --------------------------- | -| Performance | PIX code + QR generation time | < 100ms on user input | -| Accessibility | Form inputs and buttons | Keyboard navigable, labeled | -| Accessibility | QR code alt text | Meaningful description | -| i18n | All strings | Via `useTranslations()` | -| Bundle size | Added JS (Alpine + QR + PIX) | < 15 KB gzipped | -| Browser | Supported browsers | Last 2 versions, modern | - ---- - -## Constraints - -- **Technical:** Must work on Astro static site (SSG). No server-side state. - Must use Alpine.js for interactivity and Zod v4 (via `astro/zod`) for - validation. -- **Brand:** Must use PodCodar purple/violet color palette. Must match existing - DaisyUI v5 component style. -- **Language:** All user-facing text in Portuguese (pt-BR) via the i18n system. -- **Dependencies to add:** `alpinejs` (`bun add alpinejs`). Zod is already - available via `astro/zod` (bundled with Astro). QR code generation must use - an inline SVG approach implemented as a shared utility (`src/lib/qrcode.ts`) - — no external QR library. - ---- - -## Dependencies - -- **External:** None. PIX BR Code is generated client-side; no third-party API - calls. -- **Internal:** The `/contact` page must remain available for the "contact us" - fallback in FR-5. Existing `SectionHeader` and layout components from the - codebase should be reused. - ---- - -## Open Questions - -_All resolved — no open questions remain._ - -- [x] QR code: Inline SVG, implemented as `src/lib/qrcode.ts` (zero dependencies) -- [x] Copia e Cola: Truncated preview with expand toggle -- [x] Donor name/txid: Anonymous/static — no donor identifier field -- [x] Home page: Contributing page only for v1 - ---- - -## Success Metrics - -- **Completion rate:** Donors can generate a PIX code in under 30 seconds -- **Zero errors:** PIX QR codes generated by the page scan correctly in - Itaú, Nubank, PicPay, and Mercado Pago apps -- **No regressions:** Existing contributing page sections (volunteering, - partnerships) remain unchanged and functional -- **Accessibility:** All form elements pass keyboard navigation and screen - reader checks - ---- - -## Risks & Mitigations - -| Risk | Impact | Likelihood | Mitigation | -| ------------------------------------------- | ------ | ---------- | ----------------------------------------------------------- | -| PIX BR Code generation bugs (invalid QR) | High | Medium | Test against multiple banking apps; validate CRC16 | -| Alpine.js conflicts with Astro island model | Medium | Low | Use a single Astro island for the entire donation section | -| Clipboard API unavailable on some browsers | Low | Low | Fallback to `select()` + manual copy | -| Donors confused by too-large QR on mobile | Medium | Low | Responsive sizing; test on 375px viewport | -| `astro/zod` version incompatibility | Low | Low | Zod v4 API surface is stable; if issues arise, pin explicit | -| | | | zod version in package.json | - ---- - -## Appendix - -### References - -- [PIX BR Code Standard (BCB)](https://www.bcb.gov.br/content/estabilidadefinanceira/pix/Regulamento_PIX/II-ManualdePadroesparaIniciacaodoPix.pdf) -- [EMV QRCPS-MPM Specification](https://www.emvco.com/emv-technologies/qrcodes/) -- [Issue #248](https://github.com/podcodar/webapp/issues/248) -- [Current `/contributing` page](src/pages/contributing.astro) -- [i18n strings](src/i18n/ui.ts) — `contributing.donations.*` namespace - -### Test Cases - -These are the canonical test cases for validation and PIX code correctness. - -#### Amount Validation (Zod) - -| Input | Expected result | -| ---------- | ------------------------------------------- | -| `25` | Valid, PIX code generated with R$ 25.00 | -| `50` | Valid, PIX code generated with R$ 50.00 | -| `100` | Valid, PIX code generated with R$ 100.00 | -| `5` | Valid, PIX code generated with R$ 5.00 | -| `10000` | Valid, PIX code generated with R$ 10,000.00 | -| `50.50` | Valid, PIX code generated with R$ 50.50 | -| `4.99` | Invalid — below minimum (R$ 5.00) | -| `10000.01` | Invalid — contact us (> R$ 10,000) | -| `0` | Invalid — below minimum | -| `-10` | Invalid — negative amount | -| `abc` | Invalid — not a number | -| `1,000` | Invalid — wrong decimal separator | -| `50.999` | Invalid — more than 2 decimal places | -| (empty) | Invalid — required field | - -#### PIX BR Code Generation - -| Scenario | Expected behavior | -| ----------------------------- | ---------------------------------------------------- | -| Default amount (R$ 25 button) | Valid PIX string, QR renders, scan test passes | -| Custom amount (R$ 50.50) | Field `54` = `50.50`, CRC correct | -| No amount / invalid input | No PIX code generated, no QR displayed | -| Amount > R$ 10,000 | "Contact us" message, no PIX code | -| Key fields present | `00`, `26.00`, `26.01`, `53`, `58`, `59`, `60`, `63` | -| CRC16 matches | Recompute CRC and verify against field `63` | - -#### QR Code - -| Scenario | Expected behavior | -| ------------------------------- | ---------------------------------------------------- | -| Valid PIX string | QR renders as SVG, scannable | -| Banking app scan (Nubank) | Recognizes PIX, shows correct amount and recipient | -| Banking app scan (Itaú) | Recognizes PIX, shows correct amount and recipient | -| Banking app scan (PicPay) | Recognizes PIX, shows correct amount and recipient | -| Banking app scan (Mercado Pago) | Recognizes PIX, shows correct amount and recipient | -| Amount change | QR updates immediately without page reload | -| Dark mode | QR has sufficient contrast (light background/border) | - -#### Copia e Cola - -| Scenario | Expected behavior | -| ------------------------- | --------------------------------------------------- | -| Click "Copiar" | Full PIX string copied to clipboard | -| After copy | Button shows "Copiado!" for 2 seconds, then reverts | -| Truncated preview | Shows first ~40 chars + "…" + expand toggle | -| Expand toggle | Reveals full PIX string | -| Clipboard API unavailable | Falls back to text selection for manual copy | - -#### Section Layout - -| Scenario | Expected behavior | -| ---------------------------- | ------------------------------------------------------- | -| Desktop viewport (≥1024px) | Amount buttons in a row, QR + Copia e Cola side by side | -| Mobile viewport (375px) | Buttons stack vertically, QR + Copia e Cola stack | -| No JS (graceful degradation) | Section shows static message + link to `/contact` | - -### Glossary - -- **PIX:** Brazilian instant payment system operated by the Central Bank of - Brazil (BCB) -- **PIX BR Code / "Copia e Cola":** A standardized string format (EMV - QRCPS-MPM) encoding PIX payment details that can be rendered as a QR code - or pasted directly into banking apps -- **CRC16-CCITT:** A 16-bit checksum algorithm (polynomial `0x1021`) used as - the error-detection trailer in PIX BR Code strings -- **Alpine.js:** A lightweight JavaScript framework for adding interactivity - to HTML via declarative `x-*` attributes (similar spirit to htmx but - component-oriented) diff --git a/e2e/donations.spec.ts b/e2e/donations.spec.ts new file mode 100644 index 0000000..64967d2 --- /dev/null +++ b/e2e/donations.spec.ts @@ -0,0 +1,163 @@ +import { expect, test } from '@playwright/test'; + +// ────────────────────────────────────────────────────────────────────────────── +// Donations widget (embedded in /contributing) +// ────────────────────────────────────────────────────────────────────────────── + +/** + * Wait for Astro island hydration to complete before interacting with + * the SolidJS donation widget. + */ +async function waitForWidgetHydration(page: import('@playwright/test').Page) { + await page.waitForFunction(() => { + const island = document.querySelector('astro-island[client="load"]'); + return !island?.hasAttribute('ssr'); + }); +} + +test.describe('Donations page (/contributing)', () => { + test('has donation section with heading and benefits', async ({ page }) => { + await page.goto('/contributing'); + + await expect(page.getByRole('heading', { name: /doe via pix/i, level: 3 })).toBeVisible(); + await expect(page.getByText('100% do valor vai diretamente para a comunidade')).toBeVisible(); + await expect(page.getByText('Sem taxas, sem burocracia — é instantâneo')).toBeVisible(); + await expect(page.getByText('Você recebe o comprovante no app do banco')).toBeVisible(); + }); + + test('has donation widget with default R$ 25 selected', async ({ page }) => { + await page.goto('/contributing'); + await waitForWidgetHydration(page); + + // Widget title and subtitle + await expect(page.getByRole('heading', { name: /escolha um valor/i, level: 3 })).toBeVisible(); + await expect(page.getByText(/sua doação faz toda a diferença/i)).toBeVisible(); + + // Default amount R$ 25 is selected + const amount25 = page.locator('button[aria-pressed="true"]').filter({ hasText: 'R$ 25' }); + await expect(amount25).toBeVisible(); + + // PIX code is generated + const pixInput = page.locator('#pix-copia-cola'); + await expect(pixInput).toBeVisible(); + await expect(pixInput).toHaveValue(/br\.gov\.bcb\.pix/); + await expect(pixInput).toHaveValue(/doar@podcodar\.org/); + }); + + test('switches PIX code when clicking suggested amounts', async ({ page }) => { + await page.goto('/contributing'); + await waitForWidgetHydration(page); + + const pixInput = page.locator('#pix-copia-cola'); + await expect(pixInput).toBeVisible(); + + // Default is 25.00 + await expect(pixInput).toHaveValue(/25\.00/); + + // Click R$ 50 + await page.getByRole('button', { name: 'R$ 50' }).click(); + await expect(pixInput).toHaveValue(/50\.00/); + await expect(pixInput).toHaveValue(/br\.gov\.bcb\.pix/); + + // Click R$ 100 + await page.getByRole('button', { name: 'R$ 100' }).click(); + await expect(pixInput).toHaveValue(/100\.00/); + await expect(pixInput).toHaveValue(/br\.gov\.bcb\.pix/); + }); + + test('generates PIX code for custom valid amount', async ({ page }) => { + await page.goto('/contributing'); + await waitForWidgetHydration(page); + + const pixInput = page.locator('#pix-copia-cola'); + const customInput = page.locator('input[aria-label="Valor da doação"]'); + + // Clear and type a custom amount + await customInput.fill('75'); + await expect(pixInput).toHaveValue(/75\.00/); + await expect(pixInput).toHaveValue(/doar@podcodar\.org/); + }); + + test('shows validation error for amount below minimum', async ({ page }) => { + await page.goto('/contributing'); + await waitForWidgetHydration(page); + + const customInput = page.locator('input[aria-label="Valor da doação"]'); + + await customInput.fill('3'); + await expect(page.getByText(/o valor mínimo para doação é r\$ 5,00/i)).toBeVisible(); + + // PIX code should be hidden / empty state shown + const pixInput = page.locator('#pix-copia-cola'); + await expect(pixInput).toHaveValue(''); + }); + + test('shows validation error for too many decimals', async ({ page }) => { + await page.goto('/contributing'); + await waitForWidgetHydration(page); + + const customInput = page.locator('input[aria-label="Valor da doação"]'); + + // Typing non-numeric characters gets stripped immediately by the input cleaner, + // so we test the "too many decimals" validation instead. + await customInput.fill('10.234'); + await expect(page.getByText(/use no máximo duas casas decimais/i)).toBeVisible(); + }); + + test('shows empty state when input is cleared', async ({ page }) => { + await page.goto('/contributing'); + await waitForWidgetHydration(page); + + const customInput = page.locator('input[aria-label="Valor da doação"]'); + + await customInput.fill(''); + await expect(page.getByText(/digite um valor válido para gerar o qr code/i)).toBeVisible(); + }); + + test('copy button toggles to copied state', async ({ page }) => { + await page.goto('/contributing'); + await waitForWidgetHydration(page); + + const copyButton = page.locator('button[aria-label="Copiar código PIX"]'); + await expect(copyButton).toBeVisible(); + await expect(copyButton).toHaveText(/copiar/i); + + // Click copy + await copyButton.click(); + + // Should show "Copiado!" state + const copiedButton = page.locator('button[aria-label="Copiado"]'); + await expect(copiedButton).toBeVisible(); + await expect(copiedButton).toHaveText(/copiado!/i); + + // After ~2s it should revert (wait a bit longer than the timeout) + await expect(page.locator('button[aria-label="Copiar código PIX"]')).toBeVisible({ + timeout: 3000, + }); + }); + + test('QR code is visible on desktop viewport', async ({ page }) => { + await page.goto('/contributing'); + + const qrCode = page.locator('[role="img"][aria-label="QR Code para pagamento PIX"]'); + await expect(qrCode).toBeVisible(); + }); + + test('security badge is visible when PIX code is generated', async ({ page }) => { + await page.goto('/contributing'); + + await expect(page.getByText(/pagamento seguro via pix/i)).toBeVisible(); + }); + + test('navigates to contact page from donation CTA', async ({ page }) => { + await page.goto('/contributing'); + + const contactLink = page + .locator('section') + .filter({ hasText: /doe via pix/i }) + .getByRole('link', { name: /entre em contato/i }); + + await contactLink.click(); + await expect(page).toHaveURL(/\/contact/); + }); +}); diff --git a/package.json b/package.json index 122ba01..aad2da8 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,16 @@ "@astrojs/mdx": "^5.0.3", "@astrojs/rss": "^4.0.18", "@astrojs/sitemap": "^3.7.2", + "@astrojs/solid-js": "^6.0.1", "@iconify-json/lucide": "^1.2.102", "@iconify-json/simple-icons": "^1.2.79", "@tailwindcss/vite": "^4.2.2", "astro": "^6.1.8", "astro-icon": "^1.1.5", "daisyui": "^5.5.19", + "qrcode": "^1.5.4", "sharp": "^0.34.5", + "solid-js": "^1.9.12", "tailwindcss": "^4.2.2", "typescript": "^6.0.3", "wrangler": "^4.83.0" @@ -50,9 +53,10 @@ "devDependencies": { "@biomejs/biome": "^2.4.12", "@playwright/test": "^1.59.1", + "@types/qrcode": "^1.5.6", "lefthook": "^2.1.6", - "vite-tsconfig-paths": "^6.1.1", "playwright": "^1.59.1", + "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.1.4" }, "overrides": { diff --git a/src/components/donations/DonationWidget.tsx b/src/components/donations/DonationWidget.tsx new file mode 100644 index 0000000..bab1391 --- /dev/null +++ b/src/components/donations/DonationWidget.tsx @@ -0,0 +1,422 @@ +import { z } from 'astro/zod'; +import QRCode from 'qrcode'; +import { createEffect, createMemo, createSignal, Show } from 'solid-js'; +import { useTranslations } from '@/i18n/utils'; +import { generatePixString } from '@/lib/pix'; +import { + CheckIcon, + ClockIcon, + CoinIcon, + CopyIcon, + ErrorIcon, + GiftIcon, + HeartIcon, + LockIcon, + SparklesIcon, +} from './icons'; + +// ── Config ──────────────────────────────────────────────────────────────────── +const PIX_KEY = 'doar@podcodar.org'; +const MERCHANT_NAME = 'PodCodar'; +const MERCHANT_CITY = 'Belo Horizonte'; +const SUGGESTED_AMOUNTS = [25, 50, 100]; + +// ── Validation schema (astro/zod) ───────────────────────────────────────────── +const amountSchema = z + .string() + .refine((val) => /^[\d.,]+$/.test(val), { + message: 'donations.widget.validation.invalidFormat', + }) + .refine( + (val) => { + const parts = val.replace(',', '.').split('.'); + return parts.length <= 2 && (parts.length < 2 || parts[1].length <= 2); + }, + { message: 'donations.widget.validation.tooManyDecimals' } + ) + .transform((val) => parseFloat(val.replace(',', '.'))) + .pipe(z.number().min(5, { message: 'donations.widget.validation.belowMin' })); + +type ValidationResult = + | { kind: 'valid'; value: number } + | { kind: 'empty' } + | { kind: 'error'; key: string }; + +function validate(raw: string): ValidationResult { + const trimmed = raw.trim(); + if (trimmed === '') return { kind: 'empty' }; + const result = amountSchema.safeParse(trimmed); + if (!result.success) { + return { kind: 'error', key: result.error.issues[0].message }; + } + return { kind: 'valid', value: result.data }; +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── +function makeSvgResponsive(svg: string): string { + return svg.replace(/width="\d+"/, 'width="100%"').replace(/height="\d+"/, 'height="100%"'); +} + +// ── Sub-components ──────────────────────────────────────────────────────────── + +function WidgetHeader() { + const t = useTranslations(); + + return ( +
+
+ +
+
+

{t('donations.widget.title')}

+

{t('donations.widget.subtitle')}

+
+
+ ); +} + +function SuggestedAmounts(props: { amount: string; onSelect: (val: number) => void }) { + const t = useTranslations(); + + return ( +
+ {t('donations.widget.suggestedLabel')} + {SUGGESTED_AMOUNTS.map((val) => { + const isSelected = props.amount === String(val); + return ( + + ); + })} +
+ ); +} + +function AmountInput(props: { + value: string; + onInput: (e: Event) => void; + isFocused: boolean; + onFocus: () => void; + onBlur: () => void; +}) { + const t = useTranslations(); + + return ( +
+ +
+ ); +} + +function PixCopySection(props: { pixString: string; copied: boolean; onCopy: () => void }) { + const t = useTranslations(); + + return ( +
+
+
+ + +
+
+ ); +} + +function QrCorner(props: { position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' }) { + const classes = { + 'top-left': 'top-3 left-3 border-l-2 border-t-2 rounded-tl-lg', + 'top-right': 'top-3 right-3 border-r-2 border-t-2 rounded-tr-lg', + 'bottom-left': 'bottom-3 left-3 border-l-2 border-b-2 rounded-bl-lg', + 'bottom-right': 'bottom-3 right-3 border-r-2 border-b-2 rounded-br-lg', + }; + + return ( +
+ ); +} + +function QrCodePanel(props: { qrSvg: string }) { + return ( +