From f3b533b032cb6fc91cb6ebed909398c051c252f9 Mon Sep 17 00:00:00 2001 From: zefiroff Date: Tue, 9 Jun 2026 19:00:13 +0400 Subject: [PATCH] =?UTF-8?q?Quant=20Agent=20=E2=80=94=20multi-agent=20quant?= =?UTF-8?q?=20terminal=20on=20Hyperliquid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live HL data → in-browser TA → 4-agent committee → LONG/SHORT/SKIP verdict. Terminal grid command bar + interactive agent-workflow graph. Paper-trading worker reuses the browser brain (dry-run + Telegram). Forgejo static deploy. Co-Authored-By: Claude Opus 4.8 (1M context) --- .agents/skills/add-x-tweet/SKILL.md | 97 + .../add-x-tweet/scripts/fetch-tweet.mjs | 71 + .agents/skills/bklit-playground/SKILL.md | 30 + .../bklit-playground/templates/page.tsx | 7 + .agents/skills/bklit-ship/SKILL.md | 115 + .../bklit-studio-chart-performance/SKILL.md | 134 + .agents/skills/bklit-studio/SKILL.md | 107 + .agents/skills/bklit-ui/SKILL.md | 136 + .agents/skills/bklit-ui/rules/animation.md | 42 + .agents/skills/bklit-ui/rules/composition.md | 57 + .agents/skills/bklit-ui/rules/installation.md | 74 + .agents/skills/bklit-ui/rules/theming.md | 55 + .agents/skills/bklit-ui/rules/tooltips.md | 71 + .agents/skills/pr-open/SKILL.md | 299 ++ .agents/skills/turborepo/SKILL.md | 914 ++++ .agents/skills/turborepo/command/turborepo.md | 70 + .../references/best-practices/dependencies.md | 246 ++ .../references/best-practices/packages.md | 335 ++ .../references/best-practices/structure.md | 269 ++ .../turborepo/references/caching/gotchas.md | 169 + .../references/caching/remote-cache.md | 127 + .../turborepo/references/ci/github-actions.md | 162 + .../turborepo/references/ci/patterns.md | 145 + .../skills/turborepo/references/ci/vercel.md | 103 + .../turborepo/references/cli/commands.md | 297 ++ .../configuration/global-options.md | 195 + .../references/configuration/gotchas.md | 348 ++ .../references/configuration/tasks.md | 285 ++ .../references/environment/gotchas.md | 145 + .../turborepo/references/environment/modes.md | 101 + .../references/filtering/patterns.md | 152 + .agents/skills/unit-tests/SKILL.md | 111 + .agents/skills/wiki-llms-txt/SKILL.md | 129 + .claude/launch.json | 17 + .claude/skills/add-x-tweet | 1 + .claude/skills/bklit-playground | 1 + .claude/skills/bklit-ship | 1 + .claude/skills/bklit-studio | 1 + .claude/skills/bklit-studio-chart-performance | 1 + .claude/skills/bklit-ui | 1 + .claude/skills/pr-open | 1 + .claude/skills/turborepo | 1 + .claude/skills/unit-tests | 1 + .claude/skills/wiki-llms-txt | 1 + .env | 10 + .env.example | 23 + .forgejo/workflows/deploy.yml | 22 + .gitignore | 27 + .interface-design/system.md | 90 + README.md | 86 + bun.lockb | Bin 0 -> 176919 bytes components.json | 24 + docs/architecture.md | 348 ++ docs/token-strategy.md | 95 + eslint.config.js | 22 + index.html | 43 + package.json | 52 + pnpm-lock.yaml | 3827 +++++++++++++++++ proxy/factsai-worker.js | 53 + public/apple-touch-icon.png | Bin 0 -> 8157 bytes public/favicon.ico | Bin 0 -> 14510 bytes public/favicon.svg | 15 + public/icon-512.png | Bin 0 -> 30308 bytes public/og.png | Bin 0 -> 99176 bytes scripts/gen-icons.mjs | 60 + skills-lock.json | 65 + src/App.tsx | 20 + src/components/Onboarding.tsx | 124 + src/components/brand/Logo.tsx | 32 + src/components/landing/AgentFlow.tsx | 181 + src/components/landing/GlobeCanvas.tsx | 175 + src/components/landing/HeroCanvas.tsx | 146 + src/components/ui/accordion.tsx | 54 + src/components/ui/badge.tsx | 38 + src/components/ui/button.tsx | 46 + src/components/ui/dialog.tsx | 34 + src/components/ui/skeleton.tsx | 16 + src/components/ui/tabs.tsx | 46 + src/components/viz/AgentWorkflow.tsx | 396 ++ src/components/viz/ConfidenceRing.tsx | 57 + src/components/viz/LiveFeed.tsx | 177 + src/components/viz/Orchestra.tsx | 239 + src/components/viz/TVChart.tsx | 116 + src/data/seedMarket.ts | 63 + src/hooks/useHyperliquid.ts | 135 + src/hooks/useInView.ts | 70 + src/hooks/useMarketBrief.ts | 29 + src/hooks/usePrefersReducedMotion.ts | 17 + src/hooks/useScrollProgress.ts | 25 + src/lib/factsai.ts | 85 + src/lib/format.ts | 45 + src/lib/hyperliquid.ts | 112 + src/lib/seed.ts | 26 + src/lib/signals.ts | 151 + src/lib/ta.ts | 132 + src/lib/utils.ts | 7 + src/main.tsx | 10 + src/pages/Landing.tsx | 210 + src/pages/Terminal.tsx | 558 +++ src/styles/globals.css | 273 ++ tsconfig.app.json | 31 + tsconfig.json | 13 + tsconfig.node.json | 24 + vite.config.ts | 37 + worker/README.md | 72 + worker/config.ts | 48 + worker/loop.ts | 186 + worker/paper.ts | 133 + worker/store.ts | 103 + worker/telegram.ts | 66 + 110 files changed, 15045 insertions(+) create mode 100644 .agents/skills/add-x-tweet/SKILL.md create mode 100644 .agents/skills/add-x-tweet/scripts/fetch-tweet.mjs create mode 100644 .agents/skills/bklit-playground/SKILL.md create mode 100644 .agents/skills/bklit-playground/templates/page.tsx create mode 100644 .agents/skills/bklit-ship/SKILL.md create mode 100644 .agents/skills/bklit-studio-chart-performance/SKILL.md create mode 100644 .agents/skills/bklit-studio/SKILL.md create mode 100644 .agents/skills/bklit-ui/SKILL.md create mode 100644 .agents/skills/bklit-ui/rules/animation.md create mode 100644 .agents/skills/bklit-ui/rules/composition.md create mode 100644 .agents/skills/bklit-ui/rules/installation.md create mode 100644 .agents/skills/bklit-ui/rules/theming.md create mode 100644 .agents/skills/bklit-ui/rules/tooltips.md create mode 100644 .agents/skills/pr-open/SKILL.md create mode 100644 .agents/skills/turborepo/SKILL.md create mode 100644 .agents/skills/turborepo/command/turborepo.md create mode 100644 .agents/skills/turborepo/references/best-practices/dependencies.md create mode 100644 .agents/skills/turborepo/references/best-practices/packages.md create mode 100644 .agents/skills/turborepo/references/best-practices/structure.md create mode 100644 .agents/skills/turborepo/references/caching/gotchas.md create mode 100644 .agents/skills/turborepo/references/caching/remote-cache.md create mode 100644 .agents/skills/turborepo/references/ci/github-actions.md create mode 100644 .agents/skills/turborepo/references/ci/patterns.md create mode 100644 .agents/skills/turborepo/references/ci/vercel.md create mode 100644 .agents/skills/turborepo/references/cli/commands.md create mode 100644 .agents/skills/turborepo/references/configuration/global-options.md create mode 100644 .agents/skills/turborepo/references/configuration/gotchas.md create mode 100644 .agents/skills/turborepo/references/configuration/tasks.md create mode 100644 .agents/skills/turborepo/references/environment/gotchas.md create mode 100644 .agents/skills/turborepo/references/environment/modes.md create mode 100644 .agents/skills/turborepo/references/filtering/patterns.md create mode 100644 .agents/skills/unit-tests/SKILL.md create mode 100644 .agents/skills/wiki-llms-txt/SKILL.md create mode 100644 .claude/launch.json create mode 120000 .claude/skills/add-x-tweet create mode 120000 .claude/skills/bklit-playground create mode 120000 .claude/skills/bklit-ship create mode 120000 .claude/skills/bklit-studio create mode 120000 .claude/skills/bklit-studio-chart-performance create mode 120000 .claude/skills/bklit-ui create mode 120000 .claude/skills/pr-open create mode 120000 .claude/skills/turborepo create mode 120000 .claude/skills/unit-tests create mode 120000 .claude/skills/wiki-llms-txt create mode 100644 .env create mode 100644 .env.example create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 .interface-design/system.md create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 components.json create mode 100644 docs/architecture.md create mode 100644 docs/token-strategy.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 proxy/factsai-worker.js create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon.ico create mode 100644 public/favicon.svg create mode 100644 public/icon-512.png create mode 100644 public/og.png create mode 100644 scripts/gen-icons.mjs create mode 100644 skills-lock.json create mode 100644 src/App.tsx create mode 100644 src/components/Onboarding.tsx create mode 100644 src/components/brand/Logo.tsx create mode 100644 src/components/landing/AgentFlow.tsx create mode 100644 src/components/landing/GlobeCanvas.tsx create mode 100644 src/components/landing/HeroCanvas.tsx create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/viz/AgentWorkflow.tsx create mode 100644 src/components/viz/ConfidenceRing.tsx create mode 100644 src/components/viz/LiveFeed.tsx create mode 100644 src/components/viz/Orchestra.tsx create mode 100644 src/components/viz/TVChart.tsx create mode 100644 src/data/seedMarket.ts create mode 100644 src/hooks/useHyperliquid.ts create mode 100644 src/hooks/useInView.ts create mode 100644 src/hooks/useMarketBrief.ts create mode 100644 src/hooks/usePrefersReducedMotion.ts create mode 100644 src/hooks/useScrollProgress.ts create mode 100644 src/lib/factsai.ts create mode 100644 src/lib/format.ts create mode 100644 src/lib/hyperliquid.ts create mode 100644 src/lib/seed.ts create mode 100644 src/lib/signals.ts create mode 100644 src/lib/ta.ts create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 src/pages/Landing.tsx create mode 100644 src/pages/Terminal.tsx create mode 100644 src/styles/globals.css create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts create mode 100644 worker/README.md create mode 100644 worker/config.ts create mode 100644 worker/loop.ts create mode 100644 worker/paper.ts create mode 100644 worker/store.ts create mode 100644 worker/telegram.ts diff --git a/.agents/skills/add-x-tweet/SKILL.md b/.agents/skills/add-x-tweet/SKILL.md new file mode 100644 index 0000000..6e9a825 --- /dev/null +++ b/.agents/skills/add-x-tweet/SKILL.md @@ -0,0 +1,97 @@ +--- +name: add-x-tweet +description: Add X (Twitter) testimonials to the bklit homepage. Fetches username, avatar, and tweet text from a status URL and appends an entry to apps/web/lib/testimonials.ts. Use when the user shares an x.com/twitter.com tweet URL to add or replace a testimonial. +--- + +# Add X Tweet Testimonial + +Add social proof tweets to the homepage testimonial grid. + +## When to use + +- User provides one or more `https://x.com/.../status/...` URLs +- User asks to replace, prioritize, or reorder testimonials +- User says "add tweet", "new testimonial", or similar + +## Workflow + +### 1. Fetch tweet metadata + +Run the fetch script from the repo root (requires network): + +```bash +node .agents/skills/add-x-tweet/scripts/fetch-tweet.mjs "https://x.com/user/status/123..." +``` + +For multiple URLs, pass each URL or run once per URL. + +The script prints JSON: + +```json +{ + "id": "123...", + "url": "https://x.com/user/status/123...", + "author": { "name": "...", "handle": "@user", "avatar": "https://pbs.twimg.com/...", "verified": true }, + "content": "tweet text without @uixmat prefix" +} +``` + +**Fallback:** If the script fails, use Twitter oEmbed: + +```bash +curl -sL "https://publish.twitter.com/oembed?url=TWEET_URL&omit_script=1" +``` + +Parse `author_name` and HTML `

` for text. Avatar still needs fxtwitter or manual profile image URL. + +### 2. Edit `apps/web/lib/testimonials.ts` + +- Use **status id** as `id` (numeric string from URL) +- Set `url` to the canonical tweet URL (no query params) +- Run avatar through `twitterAvatarUrl()` (upgrades `_200x200` → `_400x400`) +- Strip leading `@uixmat` from content; trim whitespace +- Shorten very long tweets if they include trailing promo links (keep the praise, drop link-only tails) + +### 3. Ordering rules + +Masonry uses CSS columns (top-to-bottom per column). On `lg`, **indices 0–2** = column 1 top 3, **3–5** = column 2 top 3, **6–8** = column 3 top 3. + +- **Prioritize** when asked: place in the next open slot among indices 0–8 without duplicating ids +- **Replace** when asked: remove the old entry (match by handle or old id) before adding the new one +- Set `author.verified` from script output (`author.verification.verified` via FxTwitter) + +### 4. Collapsed grid + +The homepage shows `testimonialCollapsedCount` (6) cards before **See more**. No change needed when adding tweets unless the user asks to change visible rows. + +### 5. Verify + +```bash +cd apps/web && npx tsc --noEmit -p tsconfig.json +``` + +Open homepage — grid is 3 columns, bottom fade + **See more** expands max-height. + +## Data source + +Primary: [FxTwitter API](https://github.com/FixTweet/FxTwitter) `https://api.fxtwitter.com/{user}/status/{id}` — returns author name, screen_name, avatar_url, and text. + +Do not commit API keys; the public endpoint is used by the script only at add-tweet time. + +## Example entry + +```ts +{ + id: "2056717453816729751", + url: "https://x.com/jordienr/status/2056717453816729751", + author: { + name: "jordi", + handle: "@jordienr", + avatar: twitterAvatarUrl( + "https://pbs.twimg.com/profile_images/2053541405121769475/TUDez6zL_200x200.jpg" + ), + verified: true, + }, + content: "bklit saved my marriage", +}, +``` diff --git a/.agents/skills/add-x-tweet/scripts/fetch-tweet.mjs b/.agents/skills/add-x-tweet/scripts/fetch-tweet.mjs new file mode 100644 index 0000000..4d18ad7 --- /dev/null +++ b/.agents/skills/add-x-tweet/scripts/fetch-tweet.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/** + * Fetch X tweet metadata for homepage testimonials. + * Usage: node fetch-tweet.mjs "https://x.com/user/status/123" + */ + +const urls = process.argv.slice(2); + +if (urls.length === 0) { + console.error("Usage: node fetch-tweet.mjs [tweet-url...]"); + process.exit(1); +} + +function parseTweetUrl(url) { + const match = url.match(/(?:x|twitter)\.com\/([^/]+)\/status\/(\d+)/i); + if (!match) { + throw new Error(`Invalid tweet URL: ${url}`); + } + return { screenName: match[1], statusId: match[2] }; +} + +function stripReplyPrefix(text) { + return text.replace(/^@uixmat\s*/i, "").trim(); +} + +function upgradeAvatar(url) { + return url.replace(/_200x200\.(jpg|png|webp)$/i, "_400x400.$1"); +} + +async function fetchTweet(url) { + const { screenName, statusId } = parseTweetUrl(url); + const apiUrl = `https://api.fxtwitter.com/${screenName}/status/${statusId}`; + const res = await fetch(apiUrl, { + headers: { "User-Agent": "bklit-ui-add-x-tweet/1.0" }, + }); + + if (!res.ok) { + throw new Error(`FxTwitter ${res.status} for ${url}`); + } + + const data = await res.json(); + const tweet = data.tweet ?? data; + const author = tweet.author ?? {}; + + const canonicalUrl = `https://x.com/${screenName}/status/${statusId}`; + + const verified = author.verification?.verified === true; + + return { + id: statusId, + url: canonicalUrl, + author: { + name: author.name ?? screenName, + handle: `@${author.screen_name ?? screenName}`, + avatar: upgradeAvatar(author.avatar_url ?? author.avatar ?? ""), + verified, + }, + content: stripReplyPrefix(tweet.text ?? ""), + }; +} + +for (const url of urls) { + try { + const entry = await fetchTweet(url); + console.log(JSON.stringify(entry, null, 2)); + } catch (err) { + console.error(`Failed: ${url}`); + console.error(err.message); + process.exitCode = 1; + } +} diff --git a/.agents/skills/bklit-playground/SKILL.md b/.agents/skills/bklit-playground/SKILL.md new file mode 100644 index 0000000..f3acb52 --- /dev/null +++ b/.agents/skills/bklit-playground/SKILL.md @@ -0,0 +1,30 @@ +--- +name: bklit-playground +description: > + DEPRECATED — use bklit-studio instead. Local /playground is no longer the + chart development workflow for bklit-ui contributors. +disable-model-invocation: true +--- + +# Deprecated: bklit-playground + +**This skill is retired.** Chart prototyping and editing now happen in **Studio** only. + +## Use instead + +Read and follow **`.agents/skills/bklit-studio/SKILL.md`**. + +- Dev URL: **http://localhost:3000/studio** (`pnpm dev` from repo root) +- Optional: `?chart=line-chart` (or any slug from `packages/studio/src/chart-slugs.ts`) + +## Why + +Studio already includes the editor shell, component tree, properties, motion controls, codegen, and registry-backed previews. The gitignored `/playground` route duplicated wiring and drifted from production Studio. + +## `/playground` route + +Redirects to `/studio`. Do not scaffold `apps/web/app/playground/page.tsx` or `apps/web/components/playground/` for new chart work. + +## Shipping + +Unchanged: **`.agents/skills/bklit-ship/SKILL.md`** — prototypes should live in `packages/ui` and `packages/studio`, not under `apps/web/components/playground/`. diff --git a/.agents/skills/bklit-playground/templates/page.tsx b/.agents/skills/bklit-playground/templates/page.tsx new file mode 100644 index 0000000..0d3c715 --- /dev/null +++ b/.agents/skills/bklit-playground/templates/page.tsx @@ -0,0 +1,7 @@ +/** + * DEPRECATED — do not copy to apps/web/app/playground/page.tsx. + * + * Chart development uses Studio: http://localhost:3000/studio + * See .agents/skills/bklit-studio/SKILL.md + */ +export {}; diff --git a/.agents/skills/bklit-ship/SKILL.md b/.agents/skills/bklit-ship/SKILL.md new file mode 100644 index 0000000..fdb26c8 --- /dev/null +++ b/.agents/skills/bklit-ship/SKILL.md @@ -0,0 +1,115 @@ +--- +name: bklit-ship +description: bklit-ui monorepo contributors only — ship a chart or component from Studio prototype to production in packages/ui with docs and registry. +disable-model-invocation: true +--- + +# Bklit Ship Skill + +This skill is for **bklit-ui monorepo contributors** taking a chart or component validated in **Studio** into **production**: published in the UI package, documented, and ready for users. + +## When to use this skill + +- You cloned `bklit/bklit-ui` and have a working prototype validated in **Studio** (`/studio`) and want to ship it. +- You are ready to move from "scaffolding" to "permanent": the API and key props/variants are decided from the prototyping phase. + +--- + +## Plan: Prototype → Production + +Follow these steps in order. Treat this as a checklist; each step has concrete locations in the repo. + +### 1. Move into the UI package and export + +- **Move** or finalize chart/component source in `packages/ui/` (e.g. `packages/ui/src/charts/` for charts). Prototypes should already live here if you followed **bklit-studio**; remove any leftover copies under `apps/web/components/playground/` if present. +- **Export** the new chart/component from the package’s public API: + - For charts: add exports in `packages/ui/src/charts/index.ts`. + - If the package uses `exports` in `package.json` for specific entry points, add or update the relevant entry so the new component is importable as `@bklitui/ui/charts` (or the appropriate path). +- **Do not** add app-only chart copies under `apps/web/components/playground/` (deprecated). Studio previews belong in `packages/studio`. + +### 2. Documentation and examples (apps/web) + +Documentation lives under `apps/web/`. Do all of the following. + +- **Update existing component docs** when shipping extends an existing chart (new props, subcomponents, or behavior): + - Add new props to the relevant tables in the **parent** doc (e.g. `line-chart.mdx` for `Grid highlightRowValues`, `ChartTooltip indicatorColor`). + - Update related utility docs if needed (e.g. `content/docs/utility/grid.mdx`, `tooltip.mdx`). + - Keep the **primary docs preview unchanged** — do not swap the standard preview on an existing page for the new variant. + - Add a short section linking to a dedicated doc page when the feature warrants one (e.g. Profit/Loss → `profit-loss-line.mdx`). + +- **Chart examples (live demos on `/charts/[slug]`)** + - Add examples to the **corresponding gallery route** (e.g. profit/loss variants under `/charts/line-chart`, not only a new docs preview). + - If the feature is a new chart kind, add its slug to `apps/web/components/charts/chart-slugs.ts` and register examples in `apps/web/components/charts/chart-examples.tsx` (`CHART_NAV_ITEMS` / factory registry as appropriate). + - Reuse or mirror the prop variants you validated in the scaffolding phase. + +- **Dedicated doc page** (when shipping a new composable or chart kind) + - Add `apps/web/content/docs/components/.mdx` with frontmatter, ``, installation, usage, and props — consistent with existing component docs. + - Add the slug to `apps/web/content/docs/components/meta.json` (desktop sidebar). + - Add an entry to `apps/web/components/docs/site-header.tsx` (mobile nav). + +### 3. Studio + +- **Update the existing studio chart** when the feature is a variant of an existing type, **or add a new studio chart** when it is a distinct kind. +- Wire **all tunable props** into studio (if not already done while prototyping): + - `packages/studio/src/lib/studio-parsers.ts` — URL state keys and defaults + - `packages/studio/src/lib/registry-control-groups.ts` — control groups + - `packages/studio/src/lib/registry.tsx` — render preview + `generateCode` + - `packages/studio/src/lib/studio-components.ts` — layer tree + - Chart-type defaults in `packages/studio/src/components/studio-state-provider.tsx` when switching chart type + +### 4. Rebuild the shadcn registry + +- From the **repo root**: run `pnpm registry:build`. +- This updates `apps/web/public/r/` from `packages/ui`. Ensure new components are listed in `packages/ui/registry.json` when they should be installable via shadcn. + +### 5. Lint, format, test, and build + +Run from repo root until clean: + +```bash +pnpm lint +pnpm format +pnpm --filter @bklitui/ui test # when package logic changed +pnpm build +pnpm registry:build # if not already run in step 4 +``` + +Fix all errors; repeat until hooks pass on commit. + +### 6. Commit, push, and open a PR + +- **Commit** with a short, clear message (e.g. `feat(charts): add ProfitLossLine component`). +- **Push** the branch. +- **Open a PR** with the ship checklist filled in (see below). + +--- + +## PR checklist + +- [ ] Chart/component moved to `packages/ui` and exported +- [ ] **Existing docs updated** with new props/features (standard preview unchanged) +- [ ] Gallery examples on the correct `/charts/**` route +- [ ] Dedicated doc page + sidebar + mobile nav (if new composable/chart kind) +- [ ] **Studio** chart updated or added with all props in control groups +- [ ] Registry rebuilt (`pnpm registry:build`) +- [ ] `pnpm lint`, tests (if applicable), and `pnpm build` pass + +--- + +## File reference (quick lookup) + +| Step | Location | +|------|----------| +| Chart exports | `packages/ui/src/charts/index.ts` | +| Chart slugs | `apps/web/components/charts/chart-slugs.ts` | +| Chart examples (nav + registry) | `apps/web/components/charts/chart-examples.tsx` | +| Component docs | `apps/web/content/docs/components/.mdx` | +| Utility docs (Grid, Tooltip, …) | `apps/web/content/docs/utility/*.mdx` | +| Sidebar (desktop) | `apps/web/content/docs/components/meta.json` → `pages` | +| Mobile nav | `apps/web/components/docs/site-header.tsx` → `components` array | +| Studio registry | `packages/studio/src/lib/registry.tsx` | +| Studio components tree | `packages/studio/src/lib/studio-components.ts` | +| Studio controls | `packages/studio/src/lib/registry-control-groups.ts` | +| Studio URL state | `packages/studio/src/lib/studio-parsers.ts` | +| Registry (source) | `packages/ui/registry.json`; build output: `apps/web/public/r/` | +| Registry build | From root: `pnpm registry:build` | diff --git a/.agents/skills/bklit-studio-chart-performance/SKILL.md b/.agents/skills/bklit-studio-chart-performance/SKILL.md new file mode 100644 index 0000000..95b7429 --- /dev/null +++ b/.agents/skills/bklit-studio-chart-performance/SKILL.md @@ -0,0 +1,134 @@ +--- +name: bklit-studio-chart-performance +description: > + Reusable Studio chart performance audit and fix workflow. Use when a chart + feels sluggish in /studio (pan, slider ticks, legend hover) but siblings + like pie-chart feel fine. +--- + +# Studio chart performance + +Use when a chart feels sluggish in Studio but similar charts (e.g. pie-chart) are fine. + +## One-line rule + +Keep enter animation on paths if you need it, then **drop Motion path subscriptions** and **isolate hover** so Studio slider and legend updates don't replay expensive arc/path math across every series every frame. + +--- + +## 1. Find what re-renders on every interaction + +Studio updates `displayState` on every slider tick and on legend/slice hover. Trace: + +- Does hover live in the **same context** as data, scales, and animation config? +- Does the preview **recreate children** (`data.map`, pattern defs, motion props) every render? +- Does the chart **remount unnecessarily** (`key` tied to motion signature vs manual replay)? + +**Pattern:** Split context like cartesian / pie charts — **stable slice** (data, geometry, animation config) vs **hover slice** (`hoveredIndex`, tooltip). Consumers that don't need hover use only the stable hook (`usePieStable`, `useRingStable`, `useChartStable`, …). + +**Studio pan:** Wrap chart render in `StudioChartRender` (`packages/studio/src/components/studio-chart-render.tsx`) so camera pan / FPS counter parent updates skip the chart tree when render props are unchanged. + +--- + +## 2. Treat SVG path `d` animation as expensive + +Animating `d` with Motion / `useTransform` + d3 arc (or similar) runs layout + paint every frame, per series. + +| Prefer | Avoid | +|--------|--------| +| `transform` / `opacity` for hover (compositor-friendly) | Continuous `d` morphing after enter is done | +| Static `d` once enter finishes | Keeping Motion subscriptions on `d` for the chart's lifetime | +| Enter animation only, then static paths | Re-running enter path math on unrelated prop changes | + +**Pattern:** `useMountProgress` for enter → when progress ≥ 1 (`useEnterComplete`), render **static paths** and only animate hover with `x`/`y`/`opacity`/`scale` on a **`motion.g`** wrapper (not per-path `scale` on `motion.path`). + +Shared hook: `packages/ui/src/charts/use-enter-complete.ts` + +--- + +## 3. Memoize chart shell context + +Unmemoized provider values force all children to reconcile on every parent render. + +- Memoize the **stable context object** with explicit deps (data, arcs/radii, dimensions, callbacks). +- Memoize **hover context** on `hoveredIndex` + stable `setHoveredIndex` (`useCallback` in chart shell). +- Match **`isLoaded`** to ring/cartesian: `useEffect` + timeout, not a lazy `useState` initializer. + +Reference: `pie-context.tsx`, `ring-context.tsx`, `chart-context.tsx`, `PieChartCore` / `RingChartCore` `useMemo` on provider value. + +--- + +## 4. Studio preview–specific wins + +Chart-agnostic; apply in `packages/studio/src/components/charts/*-studio*.tsx`: + +| Win | How | +|-----|-----| +| Conditional defs | Only pass `patternDefs` / gradients when a series uses patterns | +| Memo derived data | Colored/mapped data arrays; slice/series lists (`useMemo`, deps: `dataSeed` + design fields that affect color) | +| Memo motion enter | Don't call `getStudioMotionEnterProps` inline; `useMemo` with **motion-only** deps (not full `state`) | +| Memo legend hover | `{ hoveredIndex, setHoveredIndex }` in `useMemo` — already in `studio-legend-hover.tsx` | +| Memo chart body | `memo()` wrapper; pass **primitives** (`chartKey`, `chartSize`, `data`) not whole `ctx` so pan/shell re-renders skip rebuild | +| Disable glow in Studio | `showGlow={false}` on series components | + +Reference: `pie-studio-preview.tsx`, `ring-studio-preview.tsx` + +--- + +## 5. Compare against a “fast” sibling in Studio + +Diff the slow chart against one that feels smooth in the same editor (usually **pie-chart**): + +| Check | Slow chart often has | Fast chart often has | +|-------|----------------------|----------------------| +| Shell | Inline render, extra defs | `StudioChartShell` + conditional patterns | +| Series count | Many animated paths | Fewer paths or simpler geometry | +| Hover | Context + full tree re-render | Stable subscribers; hover on `motion.g` / translate | +| Enter | Path `d` wipe per series | Static `d` after enter; transform-only hover | +| Pan | Chart tree rebuilds every frame | `StudioChartRender` memo boundary | + +--- + +## 6. Validation bar + +Before opening a PR: + +```bash +pnpm lint +pnpm --filter @bklitui/ui check-types +pnpm --filter @bklitui/studio check-types +# scoped production build when touching studio/web +``` + +Manual `/studio?chart=`: + +- [ ] Enter animation +- [ ] Hover / legend sync +- [ ] Drag geometry sliders (no unnecessary remount) +- [ ] Canvas pan (space + drag) after enter — FPS near pie-chart baseline +- [ ] Pattern/gradient mode if supported + +--- + +## Chart status (bklit-ui) + +| Chart | Slug | Status | +|-------|------|--------| +| Pie | `pie-chart` | ✅ Reference (#120) | +| Ring | `ring-chart` | ✅ Aligned to checklist (context split, static paths, `StudioChartRender`, preview memo) | +| Radar / Funnel / Choropleth | various | Partial — run checklist | +| Cartesian / scatter / live-line | various | ✅ #91 decimation + hover batching | +| Sankey | `sankey-chart` | `useTransform` on link stroke — candidate for `useEnterComplete` | +| Gauge | `gauge-chart` | Low priority (single arc) | + +--- + +## Key files + +| Area | Path | +|------|------| +| Enter-complete hook | `packages/ui/src/charts/use-enter-complete.ts` | +| Pie reference | `packages/ui/src/charts/pie-slice.tsx`, `pie-context.tsx`, `pie-studio-preview.tsx` | +| Ring | `packages/ui/src/charts/ring.tsx`, `ring-chart.tsx`, `ring-studio-preview.tsx` | +| Pan isolation | `packages/studio/src/components/studio-chart-render.tsx` | +| Registry | `packages/studio/src/lib/registry.tsx` | diff --git a/.agents/skills/bklit-studio/SKILL.md b/.agents/skills/bklit-studio/SKILL.md new file mode 100644 index 0000000..1b118bd --- /dev/null +++ b/.agents/skills/bklit-studio/SKILL.md @@ -0,0 +1,107 @@ +--- +name: bklit-studio +description: > + bklit-ui monorepo contributors only. Use automatically when building or editing + charts, tuning props/animation, or prototyping in Studio (/studio). Replaces + the deprecated local /playground route. +--- + +# Bklit Studio Skill (chart development) + +**Monorepo contributors only.** Use this skill **automatically** whenever you build, edit, or prototype a chart — before shipping via **bklit-ship**. + +Studio at **`/studio`** is the single development surface: full editor shell, component tree, properties, motion, codegen, and registry previews. Do **not** scaffold `apps/web/app/playground/`. + +## When to use (auto-trigger) + +- Build, prototype, or edit a chart (new or existing) +- Tune props, layers, data, styling, or animation +- Mentions Studio, `/studio`, registry preview, or chart controls +- Work continues from a prior playground task → use Studio instead + +## Quick start + +```bash +pnpm dev # repo root +``` + +Open **http://localhost:3000/studio** (optional query: `?chart=line-chart`). + +| Task | URL | +|------|-----| +| Edit existing chart | `/studio?chart=` — slugs in `packages/studio/src/chart-slugs.ts` | +| Profit/loss line mode | `/studio?chart=line-chart&lineChartMode=profitLoss` | + +## Editor layout + +| Region | Purpose | +|--------|---------| +| **Left** | Chart type (in full Studio), layer list, data controls, animation | +| **Center** | Canvas, rulers, resizable frame, replay/scramble | +| **Right** | Properties for the **selected layer** | + +**Pane rules:** animation → left (`motionPanel` on chart config); per-layer props → right; visibility → eye icon on layer (uses `hiddenComponents` in URL state). + +## Where to implement (existing charts) + +Edit the chart in **`packages/studio`** and **`packages/ui`** — not `apps/web/components/playground/`. + +| Concern | Path | +|---------|------| +| Chart preview + codegen | `packages/studio/src/lib/registry.tsx` (`render`, `generateCode`) | +| Layer tree (components panel) | `packages/studio/src/lib/studio-components.ts` (`resolve*Components`) | +| Control groups (properties) | `packages/studio/src/lib/registry-control-groups.ts` | +| URL / default state | `packages/studio/src/lib/studio-parsers.ts` | +| Chart UI | `packages/ui/src/charts/` | +| Slugs | `packages/studio/src/chart-slugs.ts` + `studioRegistry` in `registry.tsx` | +| Studio-only previews | `packages/studio/src/components/charts/*-studio-preview.tsx` | + +### Patterns in `registry.tsx` render + +Reuse committed helpers (do not reimplement): + +- `StudioChartShell` + `studioCartesianLegendItems` — legend grid + `showLegend` +- `StudioVisibleLayer` + `componentId` — ties render to layer visibility (`line.grid`, `line.series.0`, …) +- `getStudioCssRevealPropsForPreview` — motion / reveal +- `isStudioComponentVisible(state, componentId)` — conditional children +- `seriesStrokePropsFromState`, `fadeEdgesPropValue`, `chartTooltipPropsFromState` + +Reference implementation: `lineConfig.render` in `registry.tsx`. + +## New chart (not in registry yet) + +1. Implement component(s) in `packages/ui/src/charts/` (minimal API first). +2. Add slug to `packages/studio/src/chart-slugs.ts`. +3. Add `StudioChartConfig` to `studioRegistry` in `registry.tsx`: + - `render(state, ctx)` — preview inside `EditorChartFrame` via `StudioShell` + - `resolveComponents` in `studio-components.ts` + - `controlGroups` / `resolveControlGroups` + - `generateCode` for the code sheet +4. Open `/studio?chart=` and iterate. +5. When stable, follow **bklit-ship** for docs, gallery, shadcn registry. + +## Adding a prop or layer + +1. Add key to `StudioUrlState` / `defaultStudioState` in `studio-parsers.ts` if missing. +2. Add control(s) on the right layer in `registry-control-groups.ts` or per-component `controlGroups` in `studio-components.ts`. +3. Wire prop in `registry.tsx` `render` (and in `packages/ui` chart code). +4. If the prop affects animation → ensure `motionPanel: true` on chart config. +5. Verify in Studio: layer list, visibility toggle, properties, replay. + +## Shipping + +When the API is stable, read `.agents/skills/bklit-ship/SKILL.md` — prototype code should already live in `packages/ui` + `packages/studio`; no playground route to migrate. + +## Deprecated: `/playground` + +The local **`/playground`** route and **`bklit-playground`** skill are deprecated. `/playground` redirects to `/studio`. Do not copy `playground-editor-shell` or `apps/web/components/playground/` for new work. + +## File reference + +| Item | Path | +|------|------| +| Studio app route | `apps/web/app/studio/page.tsx` (`StudioShell`) | +| Studio package | `packages/studio/` | +| Studio skill | `.agents/skills/bklit-studio/SKILL.md` | +| Ship checklist | `.agents/skills/bklit-ship/SKILL.md` | +| Performance audit | `.agents/skills/bklit-studio-chart-performance/SKILL.md` | diff --git a/.agents/skills/bklit-ui/SKILL.md b/.agents/skills/bklit-ui/SKILL.md new file mode 100644 index 0000000..cd8ee94 --- /dev/null +++ b/.agents/skills/bklit-ui/SKILL.md @@ -0,0 +1,136 @@ +--- +name: bklit-ui +description: > + Bklit UI charts and data visualization for any project using the @bklit + shadcn registry. Install, compose, theme, and animate charts correctly. + Triggers when working with @bklitui/ui/charts, @bklit components, data + visualization, dashboards, or chart theming. Also invoke manually for + chart tasks. +allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) +--- + +# Bklit UI + +Composable chart components for React, distributed via the `@bklit` shadcn registry. Charts are installed as source into the user's project. + +> **IMPORTANT:** Run shadcn CLI commands with the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest`. + +## Current Project Context + +```json +!`npx shadcn@latest info --json` +``` + +Use the JSON above for framework, aliases, Tailwind version, installed components, and resolved paths. Confirm the `@bklit` registry is configured before adding charts. + +## Principles + +1. **Install before inventing.** Use `npx shadcn@latest add @bklit/` — charts are registry components, not hand-rolled SVG. +2. **Compose, don't flatten.** Root chart → `Grid` → series → axes → `ChartTooltip`. See [composition.md](./rules/composition.md). +3. **Theme with tokens.** Use `chartCssVars` and `--chart-*` variables — never hardcode one-off colors. See [theming.md](./rules/theming.md). +4. **Read the doc page first.** Each chart has props, data shape, and examples at `https://ui.bklit.com/docs/components/`. +5. **Browse variants.** Gallery: `https://ui.bklit.com/charts/` — Studio: `https://ui.bklit.com/studio?chart=`. + +## Critical Rules + +These rules are **always enforced**. Each links to Incorrect/Correct examples. + +### Composition → [composition.md](./rules/composition.md) + +- **Series and axes live inside the root chart** — `LineChart`, `BarChart`, `AreaChart`, etc. +- **One root per chart** — use `ComposedChart` for mixed series types. +- **Grid before series** so lines/bars render above grid lines. +- **`ChartTooltip` as a chart child** — required for crosshair and hover context. + +### Theming → [theming.md](./rules/theming.md) + +- **Use `chartCssVars`** from `@bklitui/ui/charts` instead of raw `"var(--chart-…)"` strings. +- **Series palette:** `--chart-1` … `--chart-5` for multi-series charts. +- **Tooltip surfaces:** `bg-popover text-popover-foreground` — avoids white-on-white in light mode. + +### Animation → [animation.md](./rules/animation.md) + +- **Default duration ~1100ms** for cartesian enter animations unless the doc specifies otherwise. +- **Replay:** change `revealSignature` or remount with a new `key`. +- **Live charts:** use `paused` on `LiveLineChart` to debug without stopping the rAF loop manually. + +### Tooltips → [tooltips.md](./rules/tooltips.md) + +- **Custom content via `ChartTooltip` `content` prop** or children patterns from docs. +- **`indicatorColor` function** for candlestick / dynamic crosshair colors. +- **Custom indicators:** use `useChart()` — do not track mouse globally outside chart context. + +### Installation → [installation.md](./rules/installation.md) + +- **Require `@bklit` registry** in `components.json`. +- **Install:** `npx shadcn@latest add @bklit/`. +- **Let the CLI install peer dependencies** — do not pin `@visx/*` / `motion` manually unless resolving a conflict. + +## Chart Catalog + +| Slug | Use when | Install | Docs | Gallery | +|------|----------|---------|------|---------| +| `area-chart` | Trends with filled regions under lines | `@bklit/area-chart` | [/docs/components/area-chart](https://ui.bklit.com/docs/components/area-chart) | [/charts/area-chart](https://ui.bklit.com/charts/area-chart) | +| `bar-chart` | Category comparisons, stacked or grouped bars | `@bklit/bar-chart` | [/docs/components/bar-chart](https://ui.bklit.com/docs/components/bar-chart) | [/charts/bar-chart](https://ui.bklit.com/charts/bar-chart) | +| `line-chart` | Time series, multi-line trends, markers | `@bklit/line-chart` | [/docs/components/line-chart](https://ui.bklit.com/docs/components/line-chart) | [/charts/line-chart](https://ui.bklit.com/charts/line-chart) | +| `live-line-chart` | Streaming / real-time data | `@bklit/live-line-chart` | [/docs/components/live-line-chart](https://ui.bklit.com/docs/components/live-line-chart) | [/charts/live-line-chart](https://ui.bklit.com/charts/live-line-chart) | +| `composed-chart` | Mixed bar + line (or similar) on one axis | `@bklit/composed-chart` | [/docs/components/composed-chart](https://ui.bklit.com/docs/components/composed-chart) | [/charts/composed-chart](https://ui.bklit.com/charts/composed-chart) | +| `scatter-chart` | Correlation, distribution, bubble sizing | `@bklit/scatter-chart` | [/docs/components/scatter-chart](https://ui.bklit.com/docs/components/scatter-chart) | [/charts/scatter-chart](https://ui.bklit.com/charts/scatter-chart) | +| `candlestick-chart` | OHLC financial data, brushes | `@bklit/candlestick-chart` | [/docs/components/candlestick-chart](https://ui.bklit.com/docs/components/candlestick-chart) | [/charts/candlestick-chart](https://ui.bklit.com/charts/candlestick-chart) | +| `pie-chart` | Part-to-whole slices | `@bklit/pie-chart` | [/docs/components/pie-chart](https://ui.bklit.com/docs/components/pie-chart) | [/charts/pie-chart](https://ui.bklit.com/charts/pie-chart) | +| `ring-chart` | Donut / ring KPIs | `@bklit/ring-chart` | [/docs/components/ring-chart](https://ui.bklit.com/docs/components/ring-chart) | [/charts/ring-chart](https://ui.bklit.com/charts/ring-chart) | +| `radar-chart` | Multi-axis comparison | `@bklit/radar-chart` | [/docs/components/radar-chart](https://ui.bklit.com/docs/components/radar-chart) | [/charts/radar-chart](https://ui.bklit.com/charts/radar-chart) | +| `gauge-chart` | Single-value KPI dial | `@bklit/gauge-chart` | [/docs/components/gauge-chart](https://ui.bklit.com/docs/components/gauge-chart) | [/charts/gauge-chart](https://ui.bklit.com/charts/gauge-chart) | +| `funnel-chart` | Stage conversion funnels | `@bklit/funnel-chart` | [/docs/components/funnel-chart](https://ui.bklit.com/docs/components/funnel-chart) | [/charts/funnel-chart](https://ui.bklit.com/charts/funnel-chart) | +| `sankey-chart` | Flow between nodes | `@bklit/sankey-chart` | [/docs/components/sankey-chart](https://ui.bklit.com/docs/components/sankey-chart) | [/charts/sankey-chart](https://ui.bklit.com/charts/sankey-chart) | +| `choropleth-chart` | Geo regions colored by value | `@bklit/choropleth-chart` | [/docs/components/choropleth-chart](https://ui.bklit.com/docs/components/choropleth-chart) | [/charts/choropleth-chart](https://ui.bklit.com/charts/choropleth-chart) | + +## Workflow + +1. Run `npx shadcn@latest info --json` — verify `@bklit` registry and aliases. +2. Pick a chart from the catalog (or ask the user what story the data tells). +3. Open the doc URL for data shape and props. +4. If not installed: `npx shadcn@latest add @bklit/`. +5. Compose with grid, series, axes, tooltip — apply theming tokens. +6. Point the user to the gallery or Studio URL for variant inspiration. + +## Quick Reference + +```bash +# Project info +npx shadcn@latest info --json + +# Add a chart +npx shadcn@latest add @bklit/line-chart + +# Search registries (if configured) +npx shadcn@latest search @bklit +``` + +```tsx +import { LineChart, Line, Grid, XAxis, ChartTooltip, chartCssVars } from "@bklitui/ui/charts"; + + + + + + + +``` + +## Utility docs + +- Theming: https://ui.bklit.com/docs/theming +- Grid: https://ui.bklit.com/docs/utility/grid +- Legend: https://ui.bklit.com/docs/utility/legend +- Tooltip: https://ui.bklit.com/docs/utility/tooltip +- Custom indicator: https://ui.bklit.com/docs/utility/custom-indicator +- useChart: https://ui.bklit.com/docs/utility/use-chart + +## Detailed References + +- [composition.md](./rules/composition.md) +- [theming.md](./rules/theming.md) +- [animation.md](./rules/animation.md) +- [tooltips.md](./rules/tooltips.md) +- [installation.md](./rules/installation.md) diff --git a/.agents/skills/bklit-ui/rules/animation.md b/.agents/skills/bklit-ui/rules/animation.md new file mode 100644 index 0000000..964488c --- /dev/null +++ b/.agents/skills/bklit-ui/rules/animation.md @@ -0,0 +1,42 @@ +# Animation + +## Defaults + +Most cartesian charts default to ~1100ms enter animation. Bar charts often use staggered reveals (~1.2s total) with easing `cubic-bezier(0.85, 0, 0.15, 1)`. + +## Replay enter animations + +Pass a changing `revealSignature` (or remount the chart with a new `key`) to replay mount animations after prop changes. + +### Correct + +```tsx +const [replayKey, setReplayKey] = useState(0); + + + {/* ... */} + + + +``` + +## Live charts + +- `LiveLineChart` runs its own animation loop — use `paused` to freeze updates for debugging. +- Avoid nesting heavy state updates inside the rAF path; pass stable props where possible. + +## Reduced motion + +Respect `prefers-reduced-motion` when adding custom animation wrappers around charts. Bklit charts handle reduced motion internally for enter transitions. + +## Performance + +- Prefer CSS/SVG-friendly props over re-creating large data arrays every frame. +- For streaming data, append points and trim the window instead of replacing the full array when possible. + +## Inspiration + +Browse animated variants: https://ui.bklit.com/charts/ +Tune motion interactively: https://ui.bklit.com/studio?chart= diff --git a/.agents/skills/bklit-ui/rules/composition.md b/.agents/skills/bklit-ui/rules/composition.md new file mode 100644 index 0000000..ccc9eaa --- /dev/null +++ b/.agents/skills/bklit-ui/rules/composition.md @@ -0,0 +1,57 @@ +# Chart Composition + +## Root + children + +Charts use a composable API. Always wrap series, axes, and overlays inside the root chart component. + +### Incorrect + +```tsx +

+ + +
+``` + +### Correct + +```tsx + + + + + + +``` + +## Axes and grid + +- Put `Grid` before series so lines render on top. +- Use `XAxis` / `YAxis` as siblings inside the root chart — not outside the chart context. +- For live streaming charts, use `LiveXAxis` and `LiveYAxis` inside `LiveLineChart`. + +## Tooltips and markers + +- `ChartTooltip` must be a child of the root chart so it can read hover state. +- Custom tooltip content receives chart context — prefer `useChart()` when building custom indicators. +- `ChartMarkers` and marker content render inside `ChartTooltip` when showing event callouts. + +## Multi-series charts + +- One `Line` / `Bar` / `Area` child per `dataKey`. +- Use `--chart-1` … `--chart-5` or explicit stroke/fill props for series differentiation. +- For combined line + bar, use `ComposedChart` instead of nesting unrelated roots. + +## Data shape + +- Cartesian charts: array of objects; set `xDataKey` on the root (default `"date"`). +- OHLC: use `CandlestickChart` with `{ date, open, high, low, close }`. +- Live line: `{ time, value }` points with a separate `value` prop on the root. +- Sankey / funnel / pie: follow each chart’s doc for node/link or slice shape. + +## Docs + +- Composition patterns: https://ui.bklit.com/docs/components/line-chart +- `useChart` hook: https://ui.bklit.com/docs/utility/use-chart +- Grid: https://ui.bklit.com/docs/utility/grid +- Legend: https://ui.bklit.com/docs/utility/legend diff --git a/.agents/skills/bklit-ui/rules/installation.md b/.agents/skills/bklit-ui/rules/installation.md new file mode 100644 index 0000000..b8afac0 --- /dev/null +++ b/.agents/skills/bklit-ui/rules/installation.md @@ -0,0 +1,74 @@ +# Installation + +## Prerequisites + +Bklit UI is a shadcn registry. Initialize shadcn in the project first: + +```bash +npx shadcn@latest init +``` + +## Registry namespace + +Add the `@bklit` namespace to `components.json` if it is not already present: + +```json +{ + "registries": { + "@bklit": "https://ui.bklit.com/r/{name}.json" + } +} +``` + +New projects scaffolded with Bklit often include this automatically. + +## Install a chart + +```bash +npx shadcn@latest add @bklit/line-chart +``` + +Replace `line-chart` with any chart slug from the catalog. The CLI copies source into your components directory and installs peer dependencies. + +## Verify project context + +```bash +npx shadcn@latest info --json +``` + +Use the JSON to confirm framework, aliases, Tailwind version, and which `@bklit` components are already installed. + +## Import path + +After install, import from the charts entry (path may vary by alias — check `info --json`): + +```tsx +import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@/components/ui/line-chart"; +// or from package re-exports when using the npm package directly: +import { LineChart, Line, Grid, XAxis, ChartTooltip } from "@bklitui/ui/charts"; +``` + +Prefer the import path your project’s shadcn install actually generated. + +## Common peer dependencies + +| Chart | Typical dependencies | +|-------|---------------------| +| line-chart, area-chart | `@visx/curve`, `@visx/shape`, `motion` | +| bar-chart | `@visx/gradient`, `@visx/pattern`, `@visx/shape`, `motion` | +| candlestick-chart | `@visx/scale`, `@visx/shape`, `@visx/responsive`, `d3-array`, `motion` | +| live-line-chart | `@visx/curve`, `@visx/scale`, `@visx/shape`, `@visx/responsive`, `@visx/event`, `d3-array`, `motion` | +| choropleth-chart | `@visx/geo`, `@visx/responsive`, `@visx/zoom`, `d3-geo`, `topojson-client`, `motion` | +| sankey-chart | `@visx/gradient`, `@visx/pattern`, `@visx/responsive`, `@visx/sankey`, `motion` | +| scatter-chart | `d3-scale`, `d3-array`, `motion`, `react-use-measure` | +| pie-chart, ring-chart | `@visx/responsive`, `@visx/shape`, `motion` | +| radar-chart | `@visx/responsive`, `d3-shape`, `motion` | +| funnel-chart | `motion` | +| gauge-chart | `@visx/responsive`, `motion` | + +The CLI installs these when adding a chart — do not guess versions; let `shadcn add` resolve them. + +## Docs + +- Installation: https://ui.bklit.com/docs/installation +- Per-chart install tabs: https://ui.bklit.com/docs/components/ diff --git a/.agents/skills/bklit-ui/rules/theming.md b/.agents/skills/bklit-ui/rules/theming.md new file mode 100644 index 0000000..6bbf8b5 --- /dev/null +++ b/.agents/skills/bklit-ui/rules/theming.md @@ -0,0 +1,55 @@ +# Theming + +## Use chartCssVars + +Prefer the typed `chartCssVars` export over raw CSS variable strings. + +### Incorrect + +```tsx + + +``` + +### Correct + +```tsx +import { chartCssVars } from "@bklitui/ui/charts"; + + + +``` + +## Series colors + +- Multi-series: `var(--chart-1)` through `var(--chart-5)` or theme tokens. +- Line charts: `var(--chart-line-primary)` / `var(--chart-line-secondary)` for default strokes. +- Do not hardcode hex colors unless the design explicitly requires brand colors outside the theme. + +## Tooltip and badge surfaces + +Tooltip boxes and live value badges should use shadcn popover tokens so text stays readable in light and dark mode: + +```tsx +// Tooltip content / badges +className="bg-popover text-popover-foreground" +``` + +## Dark mode + +Override chart variables in `:root` and `.dark` — do not sprinkle `dark:` on individual chart SVG elements. + +```css +:root { + --chart-background: oklch(1 0 0); + --chart-grid: oklch(0.9 0 0); +} +.dark { + --chart-background: oklch(0.145 0.004 285); + --chart-grid: oklch(0.25 0 0); +} +``` + +## Docs + +- Full theming guide: https://ui.bklit.com/docs/theming diff --git a/.agents/skills/bklit-ui/rules/tooltips.md b/.agents/skills/bklit-ui/rules/tooltips.md new file mode 100644 index 0000000..3de29ca --- /dev/null +++ b/.agents/skills/bklit-ui/rules/tooltips.md @@ -0,0 +1,71 @@ +# Tooltips + +## Default tooltip + +Use `ChartTooltip` as a child of the root chart. Defaults include crosshair, dots, and date pill where applicable. + +```tsx + + + + + +``` + +## Custom content + +```tsx + ( +
+
{formattedDate}
+
{point.users as number} users
+
+ )} +/> +``` + +## indicatorColor (candlestick and crosshair) + +Match the crosshair to the hovered datum — e.g. green/red candles: + +```tsx + + (point.close as number) >= (point.open as number) + ? "var(--chart-1)" + : "var(--chart-3)" + } + showDots={false} +/> +``` + +## Custom indicators + +For advanced crosshair/markers, use `useChart()` and the patterns in the custom indicator docs. + +### Incorrect + +```tsx +// Reading mouse position manually outside chart context +const [x, setX] = useState(0); +useEffect(() => { + window.addEventListener("mousemove", ...); +}, []); +``` + +### Correct + +```tsx +import { useChart } from "@bklitui/ui/charts"; + +function CustomIndicator() { + const { tooltipData } = useChart(); + // render from tooltipData +} +``` + +## Docs + +- ChartTooltip: https://ui.bklit.com/docs/utility/tooltip +- Custom indicators: https://ui.bklit.com/docs/utility/custom-indicator diff --git a/.agents/skills/pr-open/SKILL.md b/.agents/skills/pr-open/SKILL.md new file mode 100644 index 0000000..fa3c4ca --- /dev/null +++ b/.agents/skills/pr-open/SKILL.md @@ -0,0 +1,299 @@ +--- +name: pr-open +description: | + Open a pull request the bklit-ui way: stage and commit with pre-commit hooks, + run ultracite from the repo root, rebuild the shadcn registry, run a production test + build, fix failures, push, and create a PR with a structured summary. Use when the + user asks to commit, push, + open a PR, "ship it", or run the full pre-PR checklist. +--- + +# PR Open Skill + +End-to-end workflow for committing work and opening a merge-ready PR in **bklit-ui**. + +Read this skill when the user wants to add, commit, validate, push, and open a PR — or references `@pr-open`. + +--- + +## Before you start + +1. **Confirm branch context** + - If the user merged an earlier PR on this branch and there are new changes, branch from latest `main` instead of stacking on a stale feature branch: + ```bash + git fetch origin main + git checkout -b origin/main + ``` + - Re-apply or cherry-pick only the intended changes; do not commit unrelated `registry:build` noise (e.g. reformatted `packages/ui/registry/examples/*` unless those edits are intentional). + +2. **Never commit secrets** — skip `.env`, credentials, API keys, etc. Warn the user if they ask to commit them. + +3. **Git safety** (required) + - Do not update git config. + - Do not run destructive commands (`push --force`, `reset --hard`) unless the user explicitly asks. + - Do not skip hooks (`--no-verify`) unless the user explicitly asks. + - Do not `git commit --amend` unless all amend rules in the user's git rules are satisfied (HEAD commit is yours, not pushed, or user requested amend). + - If a commit **fails** due to a hook, fix the issue and create a **new** commit — do not amend a failed commit. + - Use HEREDOC for commit messages (see below). + - Do not push unless the user asked to push or open a PR. + +--- + +## Step 1 — Inspect changes + +Run in parallel from the repo root: + +```bash +git status +git diff +git diff --staged +git log -5 --oneline +``` + +If opening a PR, also check divergence from base: + +```bash +git fetch origin main +git log origin/main..HEAD --oneline +git diff origin/main...HEAD --stat +``` + +Draft a commit message that explains **why**, not just what. Match recent repo style (e.g. `feat(charts): …`, `fix(web): …`). + +--- + +## Step 2 — Stage and commit (pre-commit loop) + +### Stage + +```bash +git add # prefer explicit paths over blind git add -A +``` + +### Commit + +```bash +git commit -m "$(cat <<'EOF' + + + +EOF +)" +``` + +Husky **pre-commit** runs `npx ultracite fix` (see `.husky/pre-commit`). + +### If pre-commit fails or leaves unstaged fixes + +1. Read the hook output and fix every reported issue (lint correctness, nested ternaries, `biome-ignore` only when justified, etc.). +2. Re-stage affected files: `git add ` +3. Commit again with a **new** commit (or amend only if amend rules allow and the previous commit succeeded but the hook auto-modified files). + +Repeat until `git commit` succeeds **and** `git status` is clean (no leftover hook formatting). + +--- + +## Step 3 — Ultracite from repo root + +After commits are clean, run the root pnpm scripts (not `npx ultracite` directly unless fixing a one-off): + +```bash +pnpm lint # ultracite check +``` + +If check fails: + +```bash +pnpm lint:fix # ultracite fix (same as pnpm format) +``` + +Then: + +1. Review `git diff` for unexpected changes. +2. If files changed: `git add ` → `git commit -m "$(cat <<'EOF' +Fix lint issues from ultracite +EOF +)"` +3. Re-run `pnpm lint` until it passes with no fixes pending. + +Do not open a PR while `pnpm lint` fails or while lint fixes are unstaged. + +--- + +## Step 4 — Rebuild shadcn registry (required) + +**Always** run this before the production build on every PR — not only when `packages/ui` changed. Registry artifacts under `apps/web/public/r/` must stay in sync with the committed UI package. + +From the repo root: + +```bash +pnpm registry:build +``` + +This updates `apps/web/public/r/` from `packages/ui` (see `packages/ui/package.json` → `registry:build`). + +1. Review `git diff` after the build. **Include** intentional changes under `apps/web/public/r/`. +2. **Do not** commit incidental reformats on `packages/ui/registry/examples/*` unless those edits are intentional. +3. If `apps/web/public/r/` (or `packages/ui/registry.json` when you edited it) changed: + +```bash +git add apps/web/public/r packages/ui/registry.json +git commit -m "$(cat <<'EOF' +chore(registry): rebuild shadcn registry for PR +EOF +)" +``` + +4. Re-run **Step 3** (`pnpm lint`) if any source files changed outside `public/r/`. + +Do not open a PR while registry outputs are stale (uncommitted `apps/web/public/r/` diffs after `registry:build`). + +--- + +## Step 5 — Test build + +Run a production build to catch type and compile errors before the PR. + +**Default (whole monorepo):** + +```bash +pnpm build +``` + +**Web app only** (faster when changes are confined to `apps/web`): + +```bash +cd apps/web && pnpm build +``` + +If the web build OOMs in dev/CI-like environments, retry with more heap: + +```bash +cd apps/web && NODE_OPTIONS='--max-old-space-size=8192' pnpm build +``` + +**Optional but recommended** when touching TypeScript: + +```bash +pnpm check-types +``` + +### If build fails + +1. Fix the root cause (types, imports, missing registry files, etc.). +2. Re-run the failing command until it passes. +3. Commit fixes with a clear message, then re-run **Step 3** (`pnpm lint`) and **Step 4** (`pnpm registry:build`) if source files under `packages/ui` changed. + +--- + +## Step 6 — Push + +Only when the user asked to push or open a PR: + +```bash +git push -u origin HEAD +``` + +If the branch was already pushed and you added commits, `git push` is enough. + +--- + +## Step 7 — Open the PR + +Use GitHub CLI from the repo root: + +```bash +gh pr create --title "" --body "$(cat <<'EOF' +## Summary + +- +- +- + +## Test plan + +- [ ] `pnpm lint` passes at repo root +- [ ] `pnpm registry:build` run; `apps/web/public/r/` committed if updated +- [ ] `pnpm build` (or `apps/web` build for web-only changes) +- [ ] +- [ ] + +EOF +)" +``` + +Return the PR URL to the user. + +### PR title guidelines + +- Short, imperative, scoped (e.g. `Add @bklit/chart-animation registry item`) +- Match the primary commit subject when possible + +### PR body template (copy structure every time) + +```markdown +## Summary + +- <1–3 bullets: what and why> + +## Test plan + +- [ ] `pnpm lint` passes at repo root +- [ ] `pnpm registry:build` run; registry outputs committed if changed +- [ ] Production build succeeds (`pnpm build` or scoped web build) +- [ ] +- [ ] +``` + +Add extra sections only when useful: + +- **Context** — link to a merged PR or issue (e.g. "Follow-up to #70") +- **Deploy notes** — e.g. registry JSON must be live on `ui.bklit.com` for Open in v0 + +### Repo-specific PR notes + +- **Registry**: Step 4 always runs `pnpm registry:build`; commit `apps/web/public/r/` when the build updates it. +- **Do not** commit incidental reformats from `registry:build` on `packages/ui/registry/examples/*` unless intentional. +- Chart/docs work: mention manual checks for docs pages, Studio, or Open in v0 when relevant. + +--- + +## Full checklist (quick reference) + +| Step | Command / action | Must pass before next step | +|------|------------------|----------------------------| +| 1 | `git status` / `git diff` / `git log` | Understand scope | +| 2 | `git add` → `git commit` | Pre-commit hook clean; working tree clean | +| 3 | `pnpm lint` → `pnpm lint:fix` if needed → re-commit | `pnpm lint` exits 0 | +| 4 | `pnpm registry:build` → commit `apps/web/public/r/` if changed | No stale registry diff | +| 5 | `pnpm build` (and `pnpm check-types` if TS changed) | Build exits 0 | +| 6 | `git push -u origin HEAD` | Only if user requested push/PR | +| 7 | `gh pr create` with Summary + Test plan | PR URL returned | + +--- + +## Common failures + +| Failure | Fix | +|---------|-----| +| Pre-commit biome errors | Fix code; add `biome-ignore` only with a one-line justification | +| Hook fixed files but commit already succeeded | `git add` + new commit (or amend if allowed) | +| `pnpm lint` fails after commit | `pnpm lint:fix`, review diff, commit, re-run `pnpm lint` | +| `Can't resolve './animation'` in registry consumers | Add missing `@bklit/*` registry deps; run `pnpm registry:build` | +| Web build OOM | `NODE_OPTIONS='--max-old-space-size=8192'` for `apps/web` build | +| PR branch already merged | New branch from `origin/main`, re-apply only new changes | + +--- + +## Example flow + +User: "commit this and open a PR" + +1. `git status` + `git diff` + `git log -3` +2. `git add apps/web packages/ui` → `git commit` (fix pre-commit until clean) +3. `pnpm lint` → `pnpm lint:fix` if needed → commit → `pnpm lint` +4. `pnpm registry:build` → commit `apps/web/public/r/` if changed +5. `pnpm build` (fix until green) → commit if needed +6. `git push -u origin HEAD` +7. `gh pr create` with Summary + Test plan template +8. Reply with PR link and one-line summary of what was validated diff --git a/.agents/skills/turborepo/SKILL.md b/.agents/skills/turborepo/SKILL.md new file mode 100644 index 0000000..85abe40 --- /dev/null +++ b/.agents/skills/turborepo/SKILL.md @@ -0,0 +1,914 @@ +--- +name: turborepo +description: | + Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines, + dependsOn, caching, remote cache, the "turbo" CLI, --filter, --affected, CI optimization, environment + variables, internal packages, monorepo structure/best practices, and boundaries. + + Use when user: configures tasks/workflows/pipelines, creates packages, sets up + monorepo, shares code between apps, runs changed/affected packages, debugs cache, + or has apps/packages directories. +metadata: + version: 2.7.6-canary.3 +--- + +# Turborepo Skill + +Build system for JavaScript/TypeScript monorepos. Turborepo caches task outputs and runs tasks in parallel based on dependency graph. + +## IMPORTANT: Package Tasks, Not Root Tasks + +**DO NOT create Root Tasks. ALWAYS create package tasks.** + +When creating tasks/scripts/pipelines, you MUST: + +1. Add the script to each relevant package's `package.json` +2. Register the task in root `turbo.json` +3. Root `package.json` only delegates via `turbo run ` + +**DO NOT** put task logic in root `package.json`. This defeats Turborepo's parallelization. + +```json +// DO THIS: Scripts in each package +// apps/web/package.json +{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } } + +// apps/api/package.json +{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } + +// packages/ui/package.json +{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } +``` + +```json +// turbo.json - register tasks +{ + "tasks": { + "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, + "lint": {}, + "test": { "dependsOn": ["build"] } + } +} +``` + +```json +// Root package.json - ONLY delegates, no task logic +{ + "scripts": { + "build": "turbo run build", + "lint": "turbo run lint", + "test": "turbo run test" + } +} +``` + +```json +// DO NOT DO THIS - defeats parallelization +// Root package.json +{ + "scripts": { + "build": "cd apps/web && next build && cd ../api && tsc", + "lint": "eslint apps/ packages/", + "test": "vitest" + } +} +``` + +Root Tasks (`//#taskname`) are ONLY for tasks that truly cannot exist in packages (rare). + +## Secondary Rule: `turbo run` vs `turbo` + +**Always use `turbo run` when the command is written into code:** + +```json +// package.json - ALWAYS "turbo run" +{ + "scripts": { + "build": "turbo run build" + } +} +``` + +```yaml +# CI workflows - ALWAYS "turbo run" +- run: turbo run build --affected +``` + +**The shorthand `turbo ` is ONLY for one-off terminal commands** typed directly by humans or agents. Never write `turbo build` into package.json, CI, or scripts. + +## Quick Decision Trees + +### "I need to configure a task" + +``` +Configure a task? +├─ Define task dependencies → references/configuration/tasks.md +├─ Lint/check-types (parallel + caching) → Use Transit Nodes pattern (see below) +├─ Specify build outputs → references/configuration/tasks.md#outputs +├─ Handle environment variables → references/environment/README.md +├─ Set up dev/watch tasks → references/configuration/tasks.md#persistent +├─ Package-specific config → references/configuration/README.md#package-configurations +└─ Global settings (cacheDir, daemon) → references/configuration/global-options.md +``` + +### "My cache isn't working" + +``` +Cache problems? +├─ Tasks run but outputs not restored → Missing `outputs` key +├─ Cache misses unexpectedly → references/caching/gotchas.md +├─ Need to debug hash inputs → Use --summarize or --dry +├─ Want to skip cache entirely → Use --force or cache: false +├─ Remote cache not working → references/caching/remote-cache.md +└─ Environment causing misses → references/environment/gotchas.md +``` + +### "I want to run only changed packages" + +``` +Run only what changed? +├─ Changed packages + dependents (RECOMMENDED) → turbo run build --affected +├─ Custom base branch → --affected --affected-base=origin/develop +├─ Manual git comparison → --filter=...[origin/main] +└─ See all filter options → references/filtering/README.md +``` + +**`--affected` is the primary way to run only changed packages.** It automatically compares against the default branch and includes dependents. + +### "I want to filter packages" + +``` +Filter packages? +├─ Only changed packages → --affected (see above) +├─ By package name → --filter=web +├─ By directory → --filter=./apps/* +├─ Package + dependencies → --filter=web... +├─ Package + dependents → --filter=...web +└─ Complex combinations → references/filtering/patterns.md +``` + +### "Environment variables aren't working" + +``` +Environment issues? +├─ Vars not available at runtime → Strict mode filtering (default) +├─ Cache hits with wrong env → Var not in `env` key +├─ .env changes not causing rebuilds → .env not in `inputs` +├─ CI variables missing → references/environment/gotchas.md +└─ Framework vars (NEXT_PUBLIC_*) → Auto-included via inference +``` + +### "I need to set up CI" + +``` +CI setup? +├─ GitHub Actions → references/ci/github-actions.md +├─ Vercel deployment → references/ci/vercel.md +├─ Remote cache in CI → references/caching/remote-cache.md +├─ Only build changed packages → --affected flag +├─ Skip unnecessary builds → turbo-ignore (references/cli/commands.md) +└─ Skip container setup when no changes → turbo-ignore +``` + +### "I want to watch for changes during development" + +``` +Watch mode? +├─ Re-run tasks on change → turbo watch (references/watch/README.md) +├─ Dev servers with dependencies → Use `with` key (references/configuration/tasks.md#with) +├─ Restart dev server on dep change → Use `interruptible: true` +└─ Persistent dev tasks → Use `persistent: true` +``` + +### "I need to create/structure a package" + +``` +Package creation/structure? +├─ Create an internal package → references/best-practices/packages.md +├─ Repository structure → references/best-practices/structure.md +├─ Dependency management → references/best-practices/dependencies.md +├─ Best practices overview → references/best-practices/README.md +├─ JIT vs Compiled packages → references/best-practices/packages.md#compilation-strategies +└─ Sharing code between apps → references/best-practices/README.md#package-types +``` + +### "How should I structure my monorepo?" + +``` +Monorepo structure? +├─ Standard layout (apps/, packages/) → references/best-practices/README.md +├─ Package types (apps vs libraries) → references/best-practices/README.md#package-types +├─ Creating internal packages → references/best-practices/packages.md +├─ TypeScript configuration → references/best-practices/structure.md#typescript-configuration +├─ ESLint configuration → references/best-practices/structure.md#eslint-configuration +├─ Dependency management → references/best-practices/dependencies.md +└─ Enforce package boundaries → references/boundaries/README.md +``` + +### "I want to enforce architectural boundaries" + +``` +Enforce boundaries? +├─ Check for violations → turbo boundaries +├─ Tag packages → references/boundaries/README.md#tags +├─ Restrict which packages can import others → references/boundaries/README.md#rule-types +└─ Prevent cross-package file imports → references/boundaries/README.md +``` + +## Critical Anti-Patterns + +### Using `turbo` Shorthand in Code + +**`turbo run` is recommended in package.json scripts and CI pipelines.** The shorthand `turbo ` is intended for interactive terminal use. + +```json +// WRONG - using shorthand in package.json +{ + "scripts": { + "build": "turbo build", + "dev": "turbo dev" + } +} + +// CORRECT +{ + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev" + } +} +``` + +```yaml +# WRONG - using shorthand in CI +- run: turbo build --affected + +# CORRECT +- run: turbo run build --affected +``` + +### Root Scripts Bypassing Turbo + +Root `package.json` scripts MUST delegate to `turbo run`, not run tasks directly. + +```json +// WRONG - bypasses turbo entirely +{ + "scripts": { + "build": "bun build", + "dev": "bun dev" + } +} + +// CORRECT - delegates to turbo +{ + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev" + } +} +``` + +### Using `&&` to Chain Turbo Tasks + +Don't chain turbo tasks with `&&`. Let turbo orchestrate. + +```json +// WRONG - turbo task not using turbo run +{ + "scripts": { + "changeset:publish": "bun build && changeset publish" + } +} + +// CORRECT +{ + "scripts": { + "changeset:publish": "turbo run build && changeset publish" + } +} +``` + +### `prebuild` Scripts That Manually Build Dependencies + +Scripts like `prebuild` that manually build other packages bypass Turborepo's dependency graph. + +```json +// WRONG - manually building dependencies +{ + "scripts": { + "prebuild": "cd ../../packages/types && bun run build && cd ../utils && bun run build", + "build": "next build" + } +} +``` + +**However, the fix depends on whether workspace dependencies are declared:** + +1. **If dependencies ARE declared** (e.g., `"@repo/types": "workspace:*"` in package.json), remove the `prebuild` script. Turbo's `dependsOn: ["^build"]` handles this automatically. + +2. **If dependencies are NOT declared**, the `prebuild` exists because `^build` won't trigger without a dependency relationship. The fix is to: + - Add the dependency to package.json: `"@repo/types": "workspace:*"` + - Then remove the `prebuild` script + +```json +// CORRECT - declare dependency, let turbo handle build order +// package.json +{ + "dependencies": { + "@repo/types": "workspace:*", + "@repo/utils": "workspace:*" + }, + "scripts": { + "build": "next build" + } +} + +// turbo.json +{ + "tasks": { + "build": { + "dependsOn": ["^build"] + } + } +} +``` + +**Key insight:** `^build` only runs build in packages listed as dependencies. No dependency declaration = no automatic build ordering. + +### Overly Broad `globalDependencies` + +`globalDependencies` affects ALL tasks in ALL packages. Be specific. + +```json +// WRONG - heavy hammer, affects all hashes +{ + "globalDependencies": ["**/.env.*local"] +} + +// BETTER - move to task-level inputs +{ + "globalDependencies": [".env"], + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + } + } +} +``` + +### Repetitive Task Configuration + +Look for repeated configuration across tasks that can be collapsed. Turborepo supports shared configuration patterns. + +```json +// WRONG - repetitive env and inputs across tasks +{ + "tasks": { + "build": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"] + }, + "test": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"] + }, + "dev": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "cache": false, + "persistent": true + } + } +} + +// BETTER - use globalEnv and globalDependencies for shared config +{ + "globalEnv": ["API_URL", "DATABASE_URL"], + "globalDependencies": [".env*"], + "tasks": { + "build": {}, + "test": {}, + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +**When to use global vs task-level:** + +- `globalEnv` / `globalDependencies` - affects ALL tasks, use for truly shared config +- Task-level `env` / `inputs` - use when only specific tasks need it + +### NOT an Anti-Pattern: Large `env` Arrays + +A large `env` array (even 50+ variables) is **not** a problem. It usually means the user was thorough about declaring their build's environment dependencies. Do not flag this as an issue. + +### Using `--parallel` Flag + +The `--parallel` flag bypasses Turborepo's dependency graph. If tasks need parallel execution, configure `dependsOn` correctly instead. + +```bash +# WRONG - bypasses dependency graph +turbo run lint --parallel + +# CORRECT - configure tasks to allow parallel execution +# In turbo.json, set dependsOn appropriately (or use transit nodes) +turbo run lint +``` + +### Package-Specific Task Overrides in Root turbo.json + +When multiple packages need different task configurations, use **Package Configurations** (`turbo.json` in each package) instead of cluttering root `turbo.json` with `package#task` overrides. + +```json +// WRONG - root turbo.json with many package-specific overrides +{ + "tasks": { + "test": { "dependsOn": ["build"] }, + "@repo/web#test": { "outputs": ["coverage/**"] }, + "@repo/api#test": { "outputs": ["coverage/**"] }, + "@repo/utils#test": { "outputs": [] }, + "@repo/cli#test": { "outputs": [] }, + "@repo/core#test": { "outputs": [] } + } +} + +// CORRECT - use Package Configurations +// Root turbo.json - base config only +{ + "tasks": { + "test": { "dependsOn": ["build"] } + } +} + +// packages/web/turbo.json - package-specific override +{ + "extends": ["//"], + "tasks": { + "test": { "outputs": ["coverage/**"] } + } +} + +// packages/api/turbo.json +{ + "extends": ["//"], + "tasks": { + "test": { "outputs": ["coverage/**"] } + } +} +``` + +**Benefits of Package Configurations:** + +- Keeps configuration close to the code it affects +- Root turbo.json stays clean and focused on base patterns +- Easier to understand what's special about each package +- Works with `$TURBO_EXTENDS$` to inherit + extend arrays + +**When to use `package#task` in root:** + +- Single package needs a unique dependency (e.g., `"deploy": { "dependsOn": ["web#build"] }`) +- Temporary override while migrating + +See `references/configuration/README.md#package-configurations` for full details. + +### Using `../` to Traverse Out of Package in `inputs` + +Don't use relative paths like `../` to reference files outside the package. Use `$TURBO_ROOT$` instead. + +```json +// WRONG - traversing out of package +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "../shared-config.json"] + } + } +} + +// CORRECT - use $TURBO_ROOT$ for repo root +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared-config.json"] + } + } +} +``` + +### Missing `outputs` for File-Producing Tasks + +**Before flagging missing `outputs`, check what the task actually produces:** + +1. Read the package's script (e.g., `"build": "tsc"`, `"test": "vitest"`) +2. Determine if it writes files to disk or only outputs to stdout +3. Only flag if the task produces files that should be cached + +```json +// WRONG: build produces files but they're not cached +{ + "tasks": { + "build": { + "dependsOn": ["^build"] + } + } +} + +// CORRECT: build outputs are cached +{ + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + } + } +} +``` + +Common outputs by framework: + +- Next.js: `[".next/**", "!.next/cache/**"]` +- Vite/Rollup: `["dist/**"]` +- tsc: `["dist/**"]` or custom `outDir` + +**TypeScript `--noEmit` can still produce cache files:** + +When `incremental: true` in tsconfig.json, `tsc --noEmit` writes `.tsbuildinfo` files even without emitting JS. Check the tsconfig before assuming no outputs: + +```json +// If tsconfig has incremental: true, tsc --noEmit produces cache files +{ + "tasks": { + "typecheck": { + "outputs": ["node_modules/.cache/tsbuildinfo.json"] // or wherever tsBuildInfoFile points + } + } +} +``` + +To determine correct outputs for TypeScript tasks: + +1. Check if `incremental` or `composite` is enabled in tsconfig +2. Check `tsBuildInfoFile` for custom cache location (default: alongside `outDir` or in project root) +3. If no incremental mode, `tsc --noEmit` produces no files + +### `^build` vs `build` Confusion + +```json +{ + "tasks": { + // ^build = run build in DEPENDENCIES first (other packages this one imports) + "build": { + "dependsOn": ["^build"] + }, + // build (no ^) = run build in SAME PACKAGE first + "test": { + "dependsOn": ["build"] + }, + // pkg#task = specific package's task + "deploy": { + "dependsOn": ["web#build"] + } + } +} +``` + +### Environment Variables Not Hashed + +```json +// WRONG: API_URL changes won't cause rebuilds +{ + "tasks": { + "build": { + "outputs": ["dist/**"] + } + } +} + +// CORRECT: API_URL changes invalidate cache +{ + "tasks": { + "build": { + "outputs": ["dist/**"], + "env": ["API_URL", "API_KEY"] + } + } +} +``` + +### `.env` Files Not in Inputs + +Turbo does NOT load `.env` files - your framework does. But Turbo needs to know about changes: + +```json +// WRONG: .env changes don't invalidate cache +{ + "tasks": { + "build": { + "env": ["API_URL"] + } + } +} + +// CORRECT: .env file changes invalidate cache +{ + "tasks": { + "build": { + "env": ["API_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env", ".env.*"] + } + } +} +``` + +### Root `.env` File in Monorepo + +A `.env` file at the repo root is an anti-pattern — even for small monorepos or starter templates. It creates implicit coupling between packages and makes it unclear which packages depend on which variables. + +``` +// WRONG - root .env affects all packages implicitly +my-monorepo/ +├── .env # Which packages use this? +├── apps/ +│ ├── web/ +│ └── api/ +└── packages/ + +// CORRECT - .env files in packages that need them +my-monorepo/ +├── apps/ +│ ├── web/ +│ │ └── .env # Clear: web needs DATABASE_URL +│ └── api/ +│ └── .env # Clear: api needs API_KEY +└── packages/ +``` + +**Problems with root `.env`:** + +- Unclear which packages consume which variables +- All packages get all variables (even ones they don't need) +- Cache invalidation is coarse-grained (root .env change invalidates everything) +- Security risk: packages may accidentally access sensitive vars meant for others +- Bad habits start small — starter templates should model correct patterns + +**If you must share variables**, use `globalEnv` to be explicit about what's shared, and document why. + +### Strict Mode Filtering CI Variables + +By default, Turborepo filters environment variables to only those in `env`/`globalEnv`. CI variables may be missing: + +```json +// If CI scripts need GITHUB_TOKEN but it's not in env: +{ + "globalPassThroughEnv": ["GITHUB_TOKEN", "CI"], + "tasks": { ... } +} +``` + +Or use `--env-mode=loose` (not recommended for production). + +### Shared Code in Apps (Should Be a Package) + +``` +// WRONG: Shared code inside an app +apps/ + web/ + shared/ # This breaks monorepo principles! + utils.ts + +// CORRECT: Extract to a package +packages/ + utils/ + src/utils.ts +``` + +### Accessing Files Across Package Boundaries + +```typescript +// WRONG: Reaching into another package's internals +import { Button } from "../../packages/ui/src/button"; + +// CORRECT: Install and import properly +import { Button } from "@repo/ui/button"; +``` + +### Too Many Root Dependencies + +```json +// WRONG: App dependencies in root +{ + "dependencies": { + "react": "^18", + "next": "^14" + } +} + +// CORRECT: Only repo tools in root +{ + "devDependencies": { + "turbo": "latest" + } +} +``` + +## Common Task Configurations + +### Standard Build Pipeline + +```json +{ + "$schema": "https://turborepo.dev/schema.v2.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + }, + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +Add a `transit` task if you have tasks that need parallel execution with cache invalidation (see below). + +### Dev Task with `^dev` Pattern (for `turbo watch`) + +A `dev` task with `dependsOn: ["^dev"]` and `persistent: false` in root turbo.json may look unusual but is **correct for `turbo watch` workflows**: + +```json +// Root turbo.json +{ + "tasks": { + "dev": { + "dependsOn": ["^dev"], + "cache": false, + "persistent": false // Packages have one-shot dev scripts + } + } +} + +// Package turbo.json (apps/web/turbo.json) +{ + "extends": ["//"], + "tasks": { + "dev": { + "persistent": true // Apps run long-running dev servers + } + } +} +``` + +**Why this works:** + +- **Packages** (e.g., `@acme/db`, `@acme/validators`) have `"dev": "tsc"` — one-shot type generation that completes quickly +- **Apps** override with `persistent: true` for actual dev servers (Next.js, etc.) +- **`turbo watch`** re-runs the one-shot package `dev` scripts when source files change, keeping types in sync + +**Intended usage:** Run `turbo watch dev` (not `turbo run dev`). Watch mode re-executes one-shot tasks on file changes while keeping persistent tasks running. + +**Alternative pattern:** Use a separate task name like `prepare` or `generate` for one-shot dependency builds to make the intent clearer: + +```json +{ + "tasks": { + "prepare": { + "dependsOn": ["^prepare"], + "outputs": ["dist/**"] + }, + "dev": { + "dependsOn": ["prepare"], + "cache": false, + "persistent": true + } + } +} +``` + +### Transit Nodes for Parallel Tasks with Cache Invalidation + +Some tasks can run in parallel (don't need built output from dependencies) but must invalidate cache when dependency source code changes. + +**The problem with `dependsOn: ["^taskname"]`:** + +- Forces sequential execution (slow) + +**The problem with `dependsOn: []` (no dependencies):** + +- Allows parallel execution (fast) +- But cache is INCORRECT - changing dependency source won't invalidate cache + +**Transit Nodes solve both:** + +```json +{ + "tasks": { + "transit": { "dependsOn": ["^transit"] }, + "my-task": { "dependsOn": ["transit"] } + } +} +``` + +The `transit` task creates dependency relationships without matching any actual script, so tasks run in parallel with correct cache invalidation. + +**How to identify tasks that need this pattern:** Look for tasks that read source files from dependencies but don't need their build outputs. + +### With Environment Variables + +```json +{ + "globalEnv": ["NODE_ENV"], + "globalDependencies": [".env"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"], + "env": ["API_URL", "DATABASE_URL"] + } + } +} +``` + +## Reference Index + +### Configuration + +| File | Purpose | +| ------------------------------------------------------------------------------- | -------------------------------------------------------- | +| [configuration/README.md](./references/configuration/README.md) | turbo.json overview, Package Configurations | +| [configuration/tasks.md](./references/configuration/tasks.md) | dependsOn, outputs, inputs, env, cache, persistent | +| [configuration/global-options.md](./references/configuration/global-options.md) | globalEnv, globalDependencies, cacheDir, daemon, envMode | +| [configuration/gotchas.md](./references/configuration/gotchas.md) | Common configuration mistakes | + +### Caching + +| File | Purpose | +| --------------------------------------------------------------- | -------------------------------------------- | +| [caching/README.md](./references/caching/README.md) | How caching works, hash inputs | +| [caching/remote-cache.md](./references/caching/remote-cache.md) | Vercel Remote Cache, self-hosted, login/link | +| [caching/gotchas.md](./references/caching/gotchas.md) | Debugging cache misses, --summarize, --dry | + +### Environment Variables + +| File | Purpose | +| ------------------------------------------------------------- | ----------------------------------------- | +| [environment/README.md](./references/environment/README.md) | env, globalEnv, passThroughEnv | +| [environment/modes.md](./references/environment/modes.md) | Strict vs Loose mode, framework inference | +| [environment/gotchas.md](./references/environment/gotchas.md) | .env files, CI issues | + +### Filtering + +| File | Purpose | +| ----------------------------------------------------------- | ------------------------ | +| [filtering/README.md](./references/filtering/README.md) | --filter syntax overview | +| [filtering/patterns.md](./references/filtering/patterns.md) | Common filter patterns | + +### CI/CD + +| File | Purpose | +| --------------------------------------------------------- | ------------------------------- | +| [ci/README.md](./references/ci/README.md) | General CI principles | +| [ci/github-actions.md](./references/ci/github-actions.md) | Complete GitHub Actions setup | +| [ci/vercel.md](./references/ci/vercel.md) | Vercel deployment, turbo-ignore | +| [ci/patterns.md](./references/ci/patterns.md) | --affected, caching strategies | + +### CLI + +| File | Purpose | +| ----------------------------------------------- | --------------------------------------------- | +| [cli/README.md](./references/cli/README.md) | turbo run basics | +| [cli/commands.md](./references/cli/commands.md) | turbo run flags, turbo-ignore, other commands | + +### Best Practices + +| File | Purpose | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------- | +| [best-practices/README.md](./references/best-practices/README.md) | Monorepo best practices overview | +| [best-practices/structure.md](./references/best-practices/structure.md) | Repository structure, workspace config, TypeScript/ESLint setup | +| [best-practices/packages.md](./references/best-practices/packages.md) | Creating internal packages, JIT vs Compiled, exports | +| [best-practices/dependencies.md](./references/best-practices/dependencies.md) | Dependency management, installing, version sync | + +### Watch Mode + +| File | Purpose | +| ----------------------------------------------- | ----------------------------------------------- | +| [watch/README.md](./references/watch/README.md) | turbo watch, interruptible tasks, dev workflows | + +### Boundaries (Experimental) + +| File | Purpose | +| --------------------------------------------------------- | ----------------------------------------------------- | +| [boundaries/README.md](./references/boundaries/README.md) | Enforce package isolation, tag-based dependency rules | + +## Source Documentation + +This skill is based on the official Turborepo documentation at: + +- Source: `docs/site/content/docs/` in the Turborepo repository +- Live: https://turborepo.dev/docs diff --git a/.agents/skills/turborepo/command/turborepo.md b/.agents/skills/turborepo/command/turborepo.md new file mode 100644 index 0000000..615d4f1 --- /dev/null +++ b/.agents/skills/turborepo/command/turborepo.md @@ -0,0 +1,70 @@ +--- +description: Load Turborepo skill for creating workflows, tasks, and pipelines in monorepos. Use when users ask to "create a workflow", "make a task", "generate a pipeline", or set up build orchestration. +--- + +Load the Turborepo skill and help with monorepo task orchestration: creating workflows, configuring tasks, setting up pipelines, and optimizing builds. + +## Workflow + +### Step 1: Load turborepo skill + +``` +skill({ name: 'turborepo' }) +``` + +### Step 2: Identify task type from user request + +Analyze $ARGUMENTS to determine: + +- **Topic**: configuration, caching, filtering, environment, CI, or CLI +- **Task type**: new setup, debugging, optimization, or implementation + +Use decision trees in SKILL.md to select the relevant reference files. + +### Step 3: Read relevant reference files + +Based on task type, read from `references//`: + +| Task | Files to Read | +| -------------------- | --------------------------------------------------------- | +| Configure turbo.json | `configuration/README.md` + `configuration/tasks.md` | +| Debug cache issues | `caching/gotchas.md` | +| Set up remote cache | `caching/remote-cache.md` | +| Filter packages | `filtering/README.md` + `filtering/patterns.md` | +| Environment problems | `environment/gotchas.md` + `environment/modes.md` | +| Set up CI | `ci/README.md` + `ci/github-actions.md` or `ci/vercel.md` | +| CLI usage | `cli/commands.md` | + +### Step 4: Execute task + +Apply Turborepo-specific patterns from references to complete the user's request. + +**CRITICAL - When creating tasks/scripts/pipelines:** + +1. **DO NOT create Root Tasks** - Always create package tasks +2. Add scripts to each relevant package's `package.json` (e.g., `apps/web/package.json`, `packages/ui/package.json`) +3. Register the task in root `turbo.json` +4. Root `package.json` only contains `turbo run ` - never actual task logic + +**Other things to verify:** + +- `outputs` defined for cacheable tasks +- `dependsOn` uses correct syntax (`^task` vs `task`) +- Environment variables in `env` key +- `.env` files in `inputs` if used +- Use `turbo run` (not `turbo`) in package.json and CI + +### Step 5: Summarize + +``` +=== Turborepo Task Complete === + +Topic: +Files referenced: + + +``` + + +$ARGUMENTS + diff --git a/.agents/skills/turborepo/references/best-practices/dependencies.md b/.agents/skills/turborepo/references/best-practices/dependencies.md new file mode 100644 index 0000000..7a6fd2b --- /dev/null +++ b/.agents/skills/turborepo/references/best-practices/dependencies.md @@ -0,0 +1,246 @@ +# Dependency Management + +Best practices for managing dependencies in a Turborepo monorepo. + +## Core Principle: Install Where Used + +Dependencies belong in the package that uses them, not the root. + +```bash +# Good: Install in specific package +pnpm add react --filter=@repo/ui +pnpm add next --filter=web + +# Avoid: Installing in root +pnpm add react -w # Only for repo-level tools! +``` + +## Benefits of Local Installation + +### 1. Clarity + +Each package's `package.json` lists exactly what it needs: + +```json +// packages/ui/package.json +{ + "dependencies": { + "react": "^18.0.0", + "class-variance-authority": "^0.7.0" + } +} +``` + +### 2. Flexibility + +Different packages can use different versions when needed: + +```json +// packages/legacy-ui/package.json +{ "dependencies": { "react": "^17.0.0" } } + +// packages/ui/package.json +{ "dependencies": { "react": "^18.0.0" } } +``` + +### 3. Better Caching + +Installing in root changes workspace lockfile, invalidating all caches. + +### 4. Pruning Support + +`turbo prune` can remove unused dependencies for Docker images. + +## What Belongs in Root + +Only repository-level tools: + +```json +// Root package.json +{ + "devDependencies": { + "turbo": "latest", + "husky": "^8.0.0", + "lint-staged": "^15.0.0" + } +} +``` + +**NOT** application dependencies: + +- react, next, express +- lodash, axios, zod +- Testing libraries (unless truly repo-wide) + +## Installing Dependencies + +### Single Package + +```bash +# pnpm +pnpm add lodash --filter=@repo/utils + +# npm +npm install lodash --workspace=@repo/utils + +# yarn +yarn workspace @repo/utils add lodash + +# bun +cd packages/utils && bun add lodash +``` + +### Multiple Packages + +```bash +# pnpm +pnpm add jest --save-dev --filter=web --filter=@repo/ui + +# npm +npm install jest --save-dev --workspace=web --workspace=@repo/ui + +# yarn (v2+) +yarn workspaces foreach -R --from '{web,@repo/ui}' add jest --dev +``` + +### Internal Packages + +```bash +# pnpm +pnpm add @repo/ui --filter=web + +# This updates package.json: +{ + "dependencies": { + "@repo/ui": "workspace:*" + } +} +``` + +## Keeping Versions in Sync + +### Option 1: Tooling + +```bash +# syncpack - Check and fix version mismatches +npx syncpack list-mismatches +npx syncpack fix-mismatches + +# manypkg - Similar functionality +npx @manypkg/cli check +npx @manypkg/cli fix + +# sherif - Rust-based, very fast +npx sherif +``` + +### Option 2: Package Manager Commands + +```bash +# pnpm - Update everywhere +pnpm up --recursive typescript@latest + +# npm - Update in all workspaces +npm install typescript@latest --workspaces +``` + +### Option 3: pnpm Catalogs (pnpm 9.5+) + +```yaml +# pnpm-workspace.yaml +packages: + - "apps/*" + - "packages/*" + +catalog: + react: ^18.2.0 + typescript: ^5.3.0 +``` + +```json +// Any package.json +{ + "dependencies": { + "react": "catalog:" // Uses version from catalog + } +} +``` + +## Internal vs External Dependencies + +### Internal (Workspace) + +```json +// pnpm/bun +{ "@repo/ui": "workspace:*" } + +// npm/yarn +{ "@repo/ui": "*" } +``` + +Turborepo understands these relationships and orders builds accordingly. + +### External (npm Registry) + +```json +{ "lodash": "^4.17.21" } +``` + +Standard semver versioning from npm. + +## Peer Dependencies + +For library packages that expect the consumer to provide dependencies: + +```json +// packages/ui/package.json +{ + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "react": "^18.0.0", // For development/testing + "react-dom": "^18.0.0" + } +} +``` + +## Common Issues + +### "Module not found" + +1. Check the dependency is installed in the right package +2. Run `pnpm install` / `npm install` to update lockfile +3. Check exports are defined in the package + +### Version Conflicts + +Packages can use different versions - this is a feature, not a bug. But if you need consistency: + +1. Use tooling (syncpack, manypkg) +2. Use pnpm catalogs +3. Create a lint rule + +### Hoisting Issues + +Some tools expect dependencies in specific locations. Use package manager config: + +```yaml +# .npmrc (pnpm) +public-hoist-pattern[]=*eslint* +public-hoist-pattern[]=*prettier* +``` + +## Lockfile + +**Required** for: + +- Reproducible builds +- Turborepo dependency analysis +- Cache correctness + +```bash +# Commit your lockfile! +git add pnpm-lock.yaml # or package-lock.json, yarn.lock +``` diff --git a/.agents/skills/turborepo/references/best-practices/packages.md b/.agents/skills/turborepo/references/best-practices/packages.md new file mode 100644 index 0000000..29800a1 --- /dev/null +++ b/.agents/skills/turborepo/references/best-practices/packages.md @@ -0,0 +1,335 @@ +# Creating Internal Packages + +How to create and structure internal packages in your monorepo. + +## Package Creation Checklist + +1. Create directory in `packages/` +2. Add `package.json` with name and exports +3. Add source code in `src/` +4. Add `tsconfig.json` if using TypeScript +5. Install as dependency in consuming packages +6. Run package manager install to update lockfile + +## Package Compilation Strategies + +### Just-in-Time (JIT) + +Export TypeScript directly. The consuming app's bundler compiles it. + +```json +// packages/ui/package.json +{ + "name": "@repo/ui", + "exports": { + "./button": "./src/button.tsx", + "./card": "./src/card.tsx" + }, + "scripts": { + "lint": "eslint .", + "check-types": "tsc --noEmit" + } +} +``` + +**When to use:** + +- Apps use modern bundlers (Turbopack, webpack, Vite) +- You want minimal configuration +- Build times are acceptable without caching + +**Limitations:** + +- No Turborepo cache for the package itself +- Consumer must support TypeScript compilation +- Can't use TypeScript `paths` (use Node.js subpath imports instead) + +### Compiled + +Package handles its own compilation. + +```json +// packages/ui/package.json +{ + "name": "@repo/ui", + "exports": { + "./button": { + "types": "./src/button.tsx", + "default": "./dist/button.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + } +} +``` + +```json +// packages/ui/tsconfig.json +{ + "extends": "@repo/typescript-config/library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +**When to use:** + +- You want Turborepo to cache builds +- Package will be used by non-bundler tools +- You need maximum compatibility + +**Remember:** Add `dist/**` to turbo.json outputs! + +## Defining Exports + +### Multiple Entrypoints + +```json +{ + "exports": { + ".": "./src/index.ts", // @repo/ui + "./button": "./src/button.tsx", // @repo/ui/button + "./card": "./src/card.tsx", // @repo/ui/card + "./hooks": "./src/hooks/index.ts" // @repo/ui/hooks + } +} +``` + +### Conditional Exports (Compiled) + +```json +{ + "exports": { + "./button": { + "types": "./src/button.tsx", + "import": "./dist/button.mjs", + "require": "./dist/button.cjs", + "default": "./dist/button.js" + } + } +} +``` + +## Installing Internal Packages + +### Add to Consuming Package + +```json +// apps/web/package.json +{ + "dependencies": { + "@repo/ui": "workspace:*" // pnpm/bun + // "@repo/ui": "*" // npm/yarn + } +} +``` + +### Run Install + +```bash +pnpm install # Updates lockfile with new dependency +``` + +### Import and Use + +```typescript +// apps/web/src/page.tsx +import { Button } from '@repo/ui/button'; + +export default function Page() { + return ; +} +``` + +## One Purpose Per Package + +### Good Examples + +``` +packages/ +├── ui/ # Shared UI components +├── utils/ # General utilities +├── auth/ # Authentication logic +├── database/ # Database client/schemas +├── eslint-config/ # ESLint configuration +├── typescript-config/ # TypeScript configuration +└── api-client/ # Generated API client +``` + +### Avoid Mega-Packages + +``` +// BAD: One package for everything +packages/ +└── shared/ + ├── components/ + ├── utils/ + ├── hooks/ + ├── types/ + └── api/ + +// GOOD: Separate by purpose +packages/ +├── ui/ # Components +├── utils/ # Utilities +├── hooks/ # React hooks +├── types/ # Shared TypeScript types +└── api-client/ # API utilities +``` + +## Config Packages + +### TypeScript Config + +```json +// packages/typescript-config/package.json +{ + "name": "@repo/typescript-config", + "exports": { + "./base.json": "./base.json", + "./nextjs.json": "./nextjs.json", + "./library.json": "./library.json" + } +} +``` + +### ESLint Config + +```json +// packages/eslint-config/package.json +{ + "name": "@repo/eslint-config", + "exports": { + "./base": "./base.js", + "./next": "./next.js" + }, + "dependencies": { + "eslint": "^8.0.0", + "eslint-config-next": "latest" + } +} +``` + +## Common Mistakes + +### Forgetting to Export + +```json +// BAD: No exports defined +{ + "name": "@repo/ui" +} + +// GOOD: Clear exports +{ + "name": "@repo/ui", + "exports": { + "./button": "./src/button.tsx" + } +} +``` + +### Wrong Workspace Syntax + +```json +// pnpm/bun +{ "@repo/ui": "workspace:*" } // Correct + +// npm/yarn +{ "@repo/ui": "*" } // Correct +{ "@repo/ui": "workspace:*" } // Wrong for npm/yarn! +``` + +### Missing from turbo.json Outputs + +```json +// Package builds to dist/, but turbo.json doesn't know +{ + "tasks": { + "build": { + "outputs": [".next/**"] // Missing dist/**! + } + } +} + +// Correct +{ + "tasks": { + "build": { + "outputs": [".next/**", "dist/**"] + } + } +} +``` + +## TypeScript Best Practices + +### Use Node.js Subpath Imports (Not `paths`) + +TypeScript `compilerOptions.paths` breaks with JIT packages. Use Node.js subpath imports instead (TypeScript 5.4+). + +**JIT Package:** + +```json +// packages/ui/package.json +{ + "imports": { + "#*": "./src/*" + } +} +``` + +```typescript +// packages/ui/button.tsx +import { MY_STRING } from "#utils.ts"; // Uses .ts extension +``` + +**Compiled Package:** + +```json +// packages/ui/package.json +{ + "imports": { + "#*": "./dist/*" + } +} +``` + +```typescript +// packages/ui/button.tsx +import { MY_STRING } from "#utils.js"; // Uses .js extension +``` + +### Use `tsc` for Internal Packages + +For internal packages, prefer `tsc` over bundlers. Bundlers can mangle code before it reaches your app's bundler, causing hard-to-debug issues. + +### Enable Go-to-Definition + +For Compiled Packages, enable declaration maps: + +```json +// tsconfig.json +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true + } +} +``` + +This creates `.d.ts` and `.d.ts.map` files for IDE navigation. + +### No Root tsconfig.json Needed + +Each package should have its own `tsconfig.json`. A root one causes all tasks to miss cache when changed. Only use root `tsconfig.json` for non-package scripts. + +### Avoid TypeScript Project References + +They add complexity and another caching layer. Turborepo handles dependencies better. diff --git a/.agents/skills/turborepo/references/best-practices/structure.md b/.agents/skills/turborepo/references/best-practices/structure.md new file mode 100644 index 0000000..8e31de3 --- /dev/null +++ b/.agents/skills/turborepo/references/best-practices/structure.md @@ -0,0 +1,269 @@ +# Repository Structure + +Detailed guidance on structuring a Turborepo monorepo. + +## Workspace Configuration + +### pnpm (Recommended) + +```yaml +# pnpm-workspace.yaml +packages: + - "apps/*" + - "packages/*" +``` + +### npm/yarn/bun + +```json +// package.json +{ + "workspaces": ["apps/*", "packages/*"] +} +``` + +## Root package.json + +```json +{ + "name": "my-monorepo", + "private": true, + "packageManager": "pnpm@9.0.0", + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev", + "lint": "turbo run lint", + "test": "turbo run test" + }, + "devDependencies": { + "turbo": "latest" + } +} +``` + +Key points: + +- `private: true` - Prevents accidental publishing +- `packageManager` - Enforces consistent package manager version +- **Scripts only delegate to `turbo run`** - No actual build logic here! +- Minimal devDependencies (just turbo and repo tools) + +## Always Prefer Package Tasks + +**Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.** + +```json +// packages/web/package.json +{ + "scripts": { + "build": "next build", + "lint": "eslint .", + "test": "vitest", + "typecheck": "tsc --noEmit" + } +} + +// packages/api/package.json +{ + "scripts": { + "build": "tsc", + "lint": "eslint .", + "test": "vitest", + "typecheck": "tsc --noEmit" + } +} +``` + +Package tasks enable Turborepo to: + +1. **Parallelize** - Run `web#lint` and `api#lint` simultaneously +2. **Cache individually** - Each package's task output is cached separately +3. **Filter precisely** - Run `turbo run test --filter=web` for just one package + +**Root Tasks are a fallback** for tasks that truly cannot run per-package: + +```json +// AVOID unless necessary - sequential, not parallelized, can't filter +{ + "scripts": { + "lint": "eslint apps/web && eslint apps/api && eslint packages/ui" + } +} +``` + +## Root turbo.json + +```json +{ + "$schema": "https://turborepo.dev/schema.v2.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + }, + "lint": {}, + "test": { + "dependsOn": ["build"] + }, + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +## Directory Organization + +### Grouping Packages + +You can group packages by adding more workspace paths: + +```yaml +# pnpm-workspace.yaml +packages: + - "apps/*" + - "packages/*" + - "packages/config/*" # Grouped configs + - "packages/features/*" # Feature packages +``` + +This allows: + +``` +packages/ +├── ui/ +├── utils/ +├── config/ +│ ├── eslint/ +│ ├── typescript/ +│ └── tailwind/ +└── features/ + ├── auth/ + └── payments/ +``` + +### What NOT to Do + +```yaml +# BAD: Nested wildcards cause ambiguous behavior +packages: + - "packages/**" # Don't do this! +``` + +## Package Anatomy + +### Minimum Required Files + +``` +packages/ui/ +├── package.json # Required: Makes it a package +├── src/ # Source code +│ └── button.tsx +└── tsconfig.json # TypeScript config (if using TS) +``` + +### package.json Requirements + +```json +{ + "name": "@repo/ui", // Unique, namespaced name + "version": "0.0.0", // Version (can be 0.0.0 for internal) + "private": true, // Prevents accidental publishing + "exports": { // Entry points + "./button": "./src/button.tsx" + } +} +``` + +## TypeScript Configuration + +### Shared Base Config + +Create a shared TypeScript config package: + +``` +packages/ +└── typescript-config/ + ├── package.json + ├── base.json + ├── nextjs.json + └── library.json +``` + +```json +// packages/typescript-config/base.json +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "module": "ESNext", + "target": "ES2022" + } +} +``` + +### Extending in Packages + +```json +// packages/ui/tsconfig.json +{ + "extends": "@repo/typescript-config/library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +### No Root tsconfig.json + +You likely don't need a `tsconfig.json` in the workspace root. Each package should have its own config extending from the shared config package. + +## ESLint Configuration + +### Shared Config Package + +``` +packages/ +└── eslint-config/ + ├── package.json + ├── base.js + ├── next.js + └── library.js +``` + +```json +// packages/eslint-config/package.json +{ + "name": "@repo/eslint-config", + "exports": { + "./base": "./base.js", + "./next": "./next.js", + "./library": "./library.js" + } +} +``` + +### Using in Packages + +```js +// apps/web/.eslintrc.js +module.exports = { + extends: ["@repo/eslint-config/next"], +}; +``` + +## Lockfile + +A lockfile is **required** for: + +- Reproducible builds +- Turborepo to understand package dependencies +- Cache correctness + +Without a lockfile, you'll see unpredictable behavior. diff --git a/.agents/skills/turborepo/references/caching/gotchas.md b/.agents/skills/turborepo/references/caching/gotchas.md new file mode 100644 index 0000000..17d4499 --- /dev/null +++ b/.agents/skills/turborepo/references/caching/gotchas.md @@ -0,0 +1,169 @@ +# Debugging Cache Issues + +## Diagnostic Tools + +### `--summarize` + +Generates a JSON file with all hash inputs. Compare two runs to find differences. + +```bash +turbo build --summarize +# Creates .turbo/runs/.json +``` + +The summary includes: + +- Global hash and its inputs +- Per-task hashes and their inputs +- Environment variables that affected the hash + +**Comparing runs:** + +```bash +# Run twice, compare the summaries +diff .turbo/runs/.json .turbo/runs/.json +``` + +### `--dry` / `--dry=json` + +See what would run without executing anything: + +```bash +turbo build --dry +turbo build --dry=json # machine-readable output +``` + +Shows cache status for each task without running them. + +### `--force` + +Skip reading cache, re-execute all tasks: + +```bash +turbo build --force +``` + +Useful to verify tasks actually work (not just cached results). + +## Unexpected Cache Misses + +**Symptom:** Task runs when you expected a cache hit. + +### Environment Variable Changed + +Check if an env var in the `env` key changed: + +```json +{ + "tasks": { + "build": { + "env": ["API_URL", "NODE_ENV"] + } + } +} +``` + +Different `API_URL` between runs = cache miss. + +### .env File Changed + +`.env` files aren't tracked by default. Add to `inputs`: + +```json +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", ".env", ".env.local"] + } + } +} +``` + +Or use `globalDependencies` for repo-wide env files: + +```json +{ + "globalDependencies": [".env"] +} +``` + +### Lockfile Changed + +Installing/updating packages changes the global hash. + +### Source Files Changed + +Any file in the package (or in `inputs`) triggers a miss. + +### turbo.json Changed + +Config changes invalidate the global hash. + +## Incorrect Cache Hits + +**Symptom:** Cached output is stale/wrong. + +### Missing Environment Variable + +Task uses an env var not listed in `env`: + +```javascript +// build.js +const apiUrl = process.env.API_URL; // not tracked! +``` + +Fix: add to task config: + +```json +{ + "tasks": { + "build": { + "env": ["API_URL"] + } + } +} +``` + +### Missing File in Inputs + +Task reads a file outside default inputs: + +```json +{ + "tasks": { + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + "../../shared-config.json" // file outside package + ] + } + } +} +``` + +## Useful Flags + +```bash +# Only show output for cache misses +turbo build --output-logs=new-only + +# Show output for everything (debugging) +turbo build --output-logs=full + +# See why tasks are running +turbo build --verbosity=2 +``` + +## Quick Checklist + +Cache miss when expected hit: + +1. Run with `--summarize`, compare with previous run +2. Check env vars with `--dry=json` +3. Look for lockfile/config changes in git + +Cache hit when expected miss: + +1. Verify env var is in `env` array +2. Verify file is in `inputs` array +3. Check if file is outside package directory diff --git a/.agents/skills/turborepo/references/caching/remote-cache.md b/.agents/skills/turborepo/references/caching/remote-cache.md new file mode 100644 index 0000000..da76458 --- /dev/null +++ b/.agents/skills/turborepo/references/caching/remote-cache.md @@ -0,0 +1,127 @@ +# Remote Caching + +Share cache artifacts across your team and CI pipelines. + +## Benefits + +- Team members get cache hits from each other's work +- CI gets cache hits from local development (and vice versa) +- Dramatically faster CI runs after first build +- No more "works on my machine" rebuilds + +## Vercel Remote Cache + +Free, zero-config when deploying on Vercel. For local dev and other CI: + +### Local Development Setup + +```bash +# Authenticate with Vercel +npx turbo login + +# Link repo to your Vercel team +npx turbo link +``` + +This creates `.turbo/config.json` with your team info (gitignored by default). + +### CI Setup + +Set these environment variables: + +```bash +TURBO_TOKEN= +TURBO_TEAM= +``` + +Get your token from Vercel dashboard → Settings → Tokens. + +**GitHub Actions example:** + +```yaml +- name: Build + run: npx turbo build + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} +``` + +## Configuration in turbo.json + +```json +{ + "remoteCache": { + "enabled": true, + "signature": false + } +} +``` + +Options: + +- `enabled`: toggle remote cache (default: true when authenticated) +- `signature`: require artifact signing (default: false) + +## Artifact Signing + +Verify cache artifacts haven't been tampered with: + +```bash +# Set a secret key (use same key across all environments) +export TURBO_REMOTE_CACHE_SIGNATURE_KEY="your-secret-key" +``` + +Enable in config: + +```json +{ + "remoteCache": { + "signature": true + } +} +``` + +Signed artifacts can only be restored if the signature matches. + +## Self-Hosted Options + +Community implementations for running your own cache server: + +- **turbo-remote-cache** (Node.js) - supports S3, GCS, Azure +- **turborepo-remote-cache** (Go) - lightweight, S3-compatible +- **ducktape** (Rust) - high-performance option + +Configure with environment variables: + +```bash +TURBO_API=https://your-cache-server.com +TURBO_TOKEN=your-auth-token +TURBO_TEAM=your-team +``` + +## Cache Behavior Control + +```bash +# Disable remote cache for a run +turbo build --remote-cache-read-only # read but don't write +turbo build --no-cache # skip cache entirely + +# Environment variable alternative +TURBO_REMOTE_ONLY=true # only use remote, skip local +``` + +## Debugging Remote Cache + +```bash +# Verbose output shows cache operations +turbo build --verbosity=2 + +# Check if remote cache is configured +turbo config +``` + +Look for: + +- "Remote caching enabled" in output +- Upload/download messages during runs +- "cache hit, replaying output" with remote cache indicator diff --git a/.agents/skills/turborepo/references/ci/github-actions.md b/.agents/skills/turborepo/references/ci/github-actions.md new file mode 100644 index 0000000..7e5d4cc --- /dev/null +++ b/.agents/skills/turborepo/references/ci/github-actions.md @@ -0,0 +1,162 @@ +# GitHub Actions + +Complete setup guide for Turborepo with GitHub Actions. + +## Basic Workflow Structure + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Build and Test + run: turbo run build test lint +``` + +## Package Manager Setup + +### pnpm + +```yaml +- uses: pnpm/action-setup@v3 + with: + version: 9 + +- uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + +- run: pnpm install --frozen-lockfile +``` + +### Yarn + +```yaml +- uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + +- run: yarn install --frozen-lockfile +``` + +### Bun + +```yaml +- uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + +- run: bun install --frozen-lockfile +``` + +## Remote Cache Setup + +### 1. Create Vercel Access Token + +1. Go to [Vercel Dashboard](https://vercel.com/account/tokens) +2. Create a new token with appropriate scope +3. Copy the token value + +### 2. Add Secrets and Variables + +In your GitHub repository settings: + +**Secrets** (Settings > Secrets and variables > Actions > Secrets): + +- `TURBO_TOKEN`: Your Vercel access token + +**Variables** (Settings > Secrets and variables > Actions > Variables): + +- `TURBO_TEAM`: Your Vercel team slug + +### 3. Add to Workflow + +```yaml +jobs: + build: + runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} +``` + +## Alternative: actions/cache + +If you can't use remote cache, cache Turborepo's local cache directory: + +```yaml +- uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ hashFiles('**/turbo.json', '**/package-lock.json') }} + restore-keys: | + turbo-${{ runner.os }}- +``` + +Note: This is less effective than remote cache since it's per-branch. + +## Complete Example + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - uses: pnpm/action-setup@v3 + with: + version: 9 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: turbo run build --affected + + - name: Test + run: turbo run test --affected + + - name: Lint + run: turbo run lint --affected +``` diff --git a/.agents/skills/turborepo/references/ci/patterns.md b/.agents/skills/turborepo/references/ci/patterns.md new file mode 100644 index 0000000..447509a --- /dev/null +++ b/.agents/skills/turborepo/references/ci/patterns.md @@ -0,0 +1,145 @@ +# CI Optimization Patterns + +Strategies for efficient CI/CD with Turborepo. + +## PR vs Main Branch Builds + +### PR Builds: Only Affected + +Test only what changed in the PR: + +```yaml +- name: Test (PR) + if: github.event_name == 'pull_request' + run: turbo run build test --affected +``` + +### Main Branch: Full Build + +Ensure complete validation on merge: + +```yaml +- name: Test (Main) + if: github.ref == 'refs/heads/main' + run: turbo run build test +``` + +## Custom Git Ranges with --filter + +For advanced scenarios, use `--filter` with git refs: + +```bash +# Changes since specific commit +turbo run test --filter="...[abc123]" + +# Changes between refs +turbo run test --filter="...[main...HEAD]" + +# Changes in last 3 commits +turbo run test --filter="...[HEAD~3]" +``` + +## Caching Strategies + +### Remote Cache (Recommended) + +Best performance - shared across all CI runs and developers: + +```yaml +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} +``` + +### actions/cache Fallback + +When remote cache isn't available: + +```yaml +- uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ github.sha }} + restore-keys: | + turbo-${{ runner.os }}-${{ github.ref }}- + turbo-${{ runner.os }}- +``` + +Limitations: + +- Cache is branch-scoped +- PRs restore from base branch cache +- Less efficient than remote cache + +## Matrix Builds + +Test across Node versions: + +```yaml +strategy: + matrix: + node: [18, 20, 22] + +steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - run: turbo run test +``` + +## Parallelizing Across Jobs + +Split tasks into separate jobs: + +```yaml +jobs: + lint: + runs-on: ubuntu-latest + steps: + - run: turbo run lint --affected + + test: + runs-on: ubuntu-latest + steps: + - run: turbo run test --affected + + build: + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - run: turbo run build +``` + +### Cache Considerations + +When parallelizing: + +- Each job has separate cache writes +- Remote cache handles this automatically +- With actions/cache, use unique keys per job to avoid conflicts + +```yaml +- uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ github.job }}-${{ github.sha }} +``` + +## Conditional Tasks + +Skip expensive tasks on draft PRs: + +```yaml +- name: E2E Tests + if: github.event.pull_request.draft == false + run: turbo run test:e2e --affected +``` + +Or require label for full test: + +```yaml +- name: Full Test Suite + if: contains(github.event.pull_request.labels.*.name, 'full-test') + run: turbo run test +``` diff --git a/.agents/skills/turborepo/references/ci/vercel.md b/.agents/skills/turborepo/references/ci/vercel.md new file mode 100644 index 0000000..f21d41a --- /dev/null +++ b/.agents/skills/turborepo/references/ci/vercel.md @@ -0,0 +1,103 @@ +# Vercel Deployment + +Turborepo integrates seamlessly with Vercel for monorepo deployments. + +## Remote Cache + +Remote caching is **automatically enabled** when deploying to Vercel. No configuration needed - Vercel detects Turborepo and enables caching. + +This means: + +- No `TURBO_TOKEN` or `TURBO_TEAM` setup required on Vercel +- Cache is shared across all deployments +- Preview and production builds benefit from cache + +## turbo-ignore + +Skip unnecessary builds when a package hasn't changed using `turbo-ignore`. + +### Installation + +```bash +npx turbo-ignore +``` + +Or install globally in your project: + +```bash +pnpm add -D turbo-ignore +``` + +### Setup in Vercel + +1. Go to your project in Vercel Dashboard +2. Navigate to Settings > Git > Ignored Build Step +3. Select "Custom" and enter: + +```bash +npx turbo-ignore +``` + +### How It Works + +`turbo-ignore` checks if the current package (or its dependencies) changed since the last successful deployment: + +1. Compares current commit to last deployed commit +2. Uses Turborepo's dependency graph +3. Returns exit code 0 (skip) if no changes +4. Returns exit code 1 (build) if changes detected + +### Options + +```bash +# Check specific package +npx turbo-ignore web + +# Use specific comparison ref +npx turbo-ignore --fallback=HEAD~1 + +# Verbose output +npx turbo-ignore --verbose +``` + +## Environment Variables + +Set environment variables in Vercel Dashboard: + +1. Go to Project Settings > Environment Variables +2. Add variables for each environment (Production, Preview, Development) + +Common variables: + +- `DATABASE_URL` +- `API_KEY` +- Package-specific config + +## Monorepo Root Directory + +For monorepos, set the root directory in Vercel: + +1. Project Settings > General > Root Directory +2. Set to the package path (e.g., `apps/web`) + +Vercel automatically: + +- Installs dependencies from monorepo root +- Runs build from the package directory +- Detects framework settings + +## Build Command + +Vercel auto-detects `turbo run build` when `turbo.json` exists at root. + +Override if needed: + +```bash +turbo run build --filter=web +``` + +Or for production-only optimizations: + +```bash +turbo run build --filter=web --env-mode=strict +``` diff --git a/.agents/skills/turborepo/references/cli/commands.md b/.agents/skills/turborepo/references/cli/commands.md new file mode 100644 index 0000000..c1eb6b2 --- /dev/null +++ b/.agents/skills/turborepo/references/cli/commands.md @@ -0,0 +1,297 @@ +# turbo run Flags Reference + +Full docs: https://turborepo.dev/docs/reference/run + +## Package Selection + +### `--filter` / `-F` + +Select specific packages to run tasks in. + +```bash +turbo build --filter=web +turbo build -F=@repo/ui -F=@repo/utils +turbo test --filter=./apps/* +``` + +See `filtering/` for complete syntax (globs, dependencies, git ranges). + +### Task Identifier Syntax (v2.2.4+) + +Run specific package tasks directly: + +```bash +turbo run web#build # Build web package +turbo run web#build docs#lint # Multiple specific tasks +``` + +### `--affected` + +Run only in packages changed since the base branch. + +```bash +turbo build --affected +turbo test --affected --filter=./apps/* # combine with filter +``` + +**How it works:** + +- Default: compares `main...HEAD` +- In GitHub Actions: auto-detects `GITHUB_BASE_REF` +- Override base: `TURBO_SCM_BASE=development turbo build --affected` +- Override head: `TURBO_SCM_HEAD=your-branch turbo build --affected` + +**Requires git history** - shallow clones may fall back to running all tasks. + +## Execution Control + +### `--dry` / `--dry=json` + +Preview what would run without executing. + +```bash +turbo build --dry # human-readable +turbo build --dry=json # machine-readable +``` + +### `--force` + +Ignore all cached artifacts, re-run everything. + +```bash +turbo build --force +``` + +### `--concurrency` + +Limit parallel task execution. + +```bash +turbo build --concurrency=4 # max 4 tasks +turbo build --concurrency=50% # 50% of CPU cores +``` + +### `--continue` + +Keep running other tasks when one fails. + +```bash +turbo build test --continue +``` + +### `--only` + +Run only the specified task, skip its dependencies. + +```bash +turbo build --only # skip running dependsOn tasks +``` + +### `--parallel` (Discouraged) + +Ignores task graph dependencies, runs all tasks simultaneously. **Avoid using this flag**—if tasks need to run in parallel, configure `dependsOn` correctly instead. Using `--parallel` bypasses Turborepo's dependency graph, which can cause race conditions and incorrect builds. + +## Cache Control + +### `--cache` + +Fine-grained cache behavior control. + +```bash +# Default: read/write both local and remote +turbo build --cache=local:rw,remote:rw + +# Read-only local, no remote +turbo build --cache=local:r,remote: + +# Disable local, read-only remote +turbo build --cache=local:,remote:r + +# Disable all caching +turbo build --cache=local:,remote: +``` + +## Output & Debugging + +### `--graph` + +Generate task graph visualization. + +```bash +turbo build --graph # opens in browser +turbo build --graph=graph.svg # SVG file +turbo build --graph=graph.png # PNG file +turbo build --graph=graph.json # JSON data +turbo build --graph=graph.mermaid # Mermaid diagram +``` + +### `--summarize` + +Generate JSON run summary for debugging. + +```bash +turbo build --summarize +# creates .turbo/runs/.json +``` + +### `--output-logs` + +Control log output verbosity. + +```bash +turbo build --output-logs=full # all logs (default) +turbo build --output-logs=new-only # only cache misses +turbo build --output-logs=errors-only # only failures +turbo build --output-logs=none # silent +``` + +### `--profile` + +Generate Chrome tracing profile for performance analysis. + +```bash +turbo build --profile=profile.json +# open chrome://tracing and load the file +``` + +### `--verbosity` / `-v` + +Control turbo's own log level. + +```bash +turbo build -v # verbose +turbo build -vv # more verbose +turbo build -vvv # maximum verbosity +``` + +## Environment + +### `--env-mode` + +Control environment variable handling. + +```bash +turbo build --env-mode=strict # only declared env vars (default) +turbo build --env-mode=loose # include all env vars in hash +``` + +## UI + +### `--ui` + +Select output interface. + +```bash +turbo build --ui=tui # interactive terminal UI (default in TTY) +turbo build --ui=stream # streaming logs (default in CI) +``` + +--- + +# turbo-ignore + +Full docs: https://turborepo.dev/docs/reference/turbo-ignore + +Skip CI work when nothing relevant changed. Useful for skipping container setup. + +## Basic Usage + +```bash +# Check if build is needed for current package (uses Automatic Package Scoping) +npx turbo-ignore + +# Check specific package +npx turbo-ignore web + +# Check specific task +npx turbo-ignore --task=test +``` + +## Exit Codes + +- `0`: No changes detected - skip CI work +- `1`: Changes detected - proceed with CI + +## CI Integration Example + +```yaml +# GitHub Actions +- name: Check for changes + id: turbo-ignore + run: npx turbo-ignore web + continue-on-error: true + +- name: Build + if: steps.turbo-ignore.outcome == 'failure' # changes detected + run: pnpm build +``` + +## Comparison Depth + +Default: compares to parent commit (`HEAD^1`). + +```bash +# Compare to specific commit +npx turbo-ignore --fallback=abc123 + +# Compare to branch +npx turbo-ignore --fallback=main +``` + +--- + +# Other Commands + +## turbo boundaries + +Check workspace violations (experimental). + +```bash +turbo boundaries +``` + +See `references/boundaries/` for configuration. + +## turbo watch + +Re-run tasks on file changes. + +```bash +turbo watch build test +``` + +See `references/watch/` for details. + +## turbo prune + +Create sparse checkout for Docker. + +```bash +turbo prune web --docker +``` + +## turbo link / unlink + +Connect/disconnect Remote Cache. + +```bash +turbo link # connect to Vercel Remote Cache +turbo unlink # disconnect +``` + +## turbo login / logout + +Authenticate with Remote Cache provider. + +```bash +turbo login # authenticate +turbo logout # log out +``` + +## turbo generate + +Scaffold new packages. + +```bash +turbo generate +``` diff --git a/.agents/skills/turborepo/references/configuration/global-options.md b/.agents/skills/turborepo/references/configuration/global-options.md new file mode 100644 index 0000000..8394c1a --- /dev/null +++ b/.agents/skills/turborepo/references/configuration/global-options.md @@ -0,0 +1,195 @@ +# Global Options Reference + +Options that affect all tasks. Full docs: https://turborepo.dev/docs/reference/configuration + +## globalEnv + +Environment variables affecting all task hashes. + +```json +{ + "globalEnv": ["CI", "NODE_ENV", "VERCEL_*"] +} +``` + +Use for variables that should invalidate all caches when changed. + +## globalDependencies + +Files that affect all task hashes. + +```json +{ + "globalDependencies": [ + "tsconfig.json", + ".env", + "pnpm-lock.yaml" + ] +} +``` + +Lockfile is included by default. Add shared configs here. + +## globalPassThroughEnv + +Variables available to tasks but not included in hash. + +```json +{ + "globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"] +} +``` + +Use for credentials that shouldn't affect cache keys. + +## cacheDir + +Custom cache location. Default: `node_modules/.cache/turbo`. + +```json +{ + "cacheDir": ".turbo/cache" +} +``` + +## daemon + +Background process for faster subsequent runs. Default: `true`. + +```json +{ + "daemon": false +} +``` + +Disable in CI or when debugging. + +## envMode + +How unspecified env vars are handled. Default: `"strict"`. + +```json +{ + "envMode": "strict" // Only specified vars available + // or + "envMode": "loose" // All vars pass through +} +``` + +Strict mode catches missing env declarations. + +## ui + +Terminal UI mode. Default: `"stream"`. + +```json +{ + "ui": "tui" // Interactive terminal UI + // or + "ui": "stream" // Traditional streaming logs +} +``` + +TUI provides better UX for parallel tasks. + +## remoteCache + +Configure remote caching. + +```json +{ + "remoteCache": { + "enabled": true, + "signature": true, + "timeout": 30, + "uploadTimeout": 60 + } +} +``` + +| Option | Default | Description | +| --------------- | ---------------------- | ------------------------------------------------------ | +| `enabled` | `true` | Enable/disable remote caching | +| `signature` | `false` | Sign artifacts with `TURBO_REMOTE_CACHE_SIGNATURE_KEY` | +| `preflight` | `false` | Send OPTIONS request before cache requests | +| `timeout` | `30` | Timeout in seconds for cache operations | +| `uploadTimeout` | `60` | Timeout in seconds for uploads | +| `apiUrl` | `"https://vercel.com"` | Remote cache API endpoint | +| `loginUrl` | `"https://vercel.com"` | Login endpoint | +| `teamId` | - | Team ID (must start with `team_`) | +| `teamSlug` | - | Team slug for querystring | + +See https://turborepo.dev/docs/core-concepts/remote-caching for setup. + +## concurrency + +Default: `"10"` + +Limit parallel task execution. + +```json +{ + "concurrency": "4" // Max 4 tasks at once + // or + "concurrency": "50%" // 50% of available CPUs +} +``` + +## futureFlags + +Enable experimental features that will become default in future versions. + +```json +{ + "futureFlags": { + "errorsOnlyShowHash": true + } +} +``` + +### `errorsOnlyShowHash` + +When using `outputLogs: "errors-only"`, show task hashes on start/completion: + +- Cache miss: `cache miss, executing (only logging errors)` +- Cache hit: `cache hit, replaying logs (no errors) ` + +## noUpdateNotifier + +Disable update notifications when new turbo versions are available. + +```json +{ + "noUpdateNotifier": true +} +``` + +## dangerouslyDisablePackageManagerCheck + +Bypass the `packageManager` field requirement. Use for incremental migration. + +```json +{ + "dangerouslyDisablePackageManagerCheck": true +} +``` + +**Warning**: Unstable lockfiles can cause unpredictable behavior. + +## Git Worktree Cache Sharing (Pre-release) + +When working in Git worktrees, Turborepo automatically shares local cache between the main worktree and linked worktrees. + +**How it works:** + +- Detects worktree configuration +- Redirects cache to main worktree's `.turbo/cache` +- Works alongside Remote Cache + +**Benefits:** + +- Cache hits across branches +- Reduced disk usage +- Faster branch switching + +**Disabled by**: Setting explicit `cacheDir` in turbo.json. diff --git a/.agents/skills/turborepo/references/configuration/gotchas.md b/.agents/skills/turborepo/references/configuration/gotchas.md new file mode 100644 index 0000000..225bd39 --- /dev/null +++ b/.agents/skills/turborepo/references/configuration/gotchas.md @@ -0,0 +1,348 @@ +# Configuration Gotchas + +Common mistakes and how to fix them. + +## #1 Root Scripts Not Using `turbo run` + +Root `package.json` scripts for turbo tasks MUST use `turbo run`, not direct commands. + +```json +// WRONG - bypasses turbo, no parallelization or caching +{ + "scripts": { + "build": "bun build", + "dev": "bun dev" + } +} + +// CORRECT - delegates to turbo +{ + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev" + } +} +``` + +**Why this matters:** Running `bun build` or `npm run build` at root bypasses Turborepo entirely - no parallelization, no caching, no dependency graph awareness. + +## #2 Using `&&` to Chain Turbo Tasks + +Don't use `&&` to chain tasks that turbo should orchestrate. + +```json +// WRONG - changeset:publish chains turbo task with non-turbo command +{ + "scripts": { + "changeset:publish": "bun build && changeset publish" + } +} + +// CORRECT - use turbo run, let turbo handle dependencies +{ + "scripts": { + "changeset:publish": "turbo run build && changeset publish" + } +} +``` + +If the second command (`changeset publish`) depends on build outputs, the turbo task should run through turbo to get caching and parallelization benefits. + +## #3 Overly Broad globalDependencies + +`globalDependencies` affects hash for ALL tasks in ALL packages. Be specific. + +```json +// WRONG - affects all hashes +{ + "globalDependencies": ["**/.env.*local"] +} + +// CORRECT - move to specific tasks that need it +{ + "globalDependencies": [".env"], + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + } + } +} +``` + +**Why this matters:** `**/.env.*local` matches .env files in ALL packages, causing unnecessary cache invalidation. Instead: + +- Use `globalDependencies` only for truly global files (root `.env`) +- Use task-level `inputs` for package-specific .env files with `$TURBO_DEFAULT$` to preserve default behavior + +## #4 Repetitive Task Configuration + +Look for repeated configuration across tasks that can be collapsed. + +```json +// WRONG - repetitive env and inputs across tasks +{ + "tasks": { + "build": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"] + }, + "test": { + "env": ["API_URL", "DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env*"] + } + } +} + +// BETTER - use globalEnv and globalDependencies +{ + "globalEnv": ["API_URL", "DATABASE_URL"], + "globalDependencies": [".env*"], + "tasks": { + "build": {}, + "test": {} + } +} +``` + +**When to use global vs task-level:** + +- `globalEnv` / `globalDependencies` - affects ALL tasks, use for truly shared config +- Task-level `env` / `inputs` - use when only specific tasks need it + +## #5 Using `../` to Traverse Out of Package in `inputs` + +Don't use relative paths like `../` to reference files outside the package. Use `$TURBO_ROOT$` instead. + +```json +// WRONG - traversing out of package +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "../shared-config.json"] + } + } +} + +// CORRECT - use $TURBO_ROOT$ for repo root +{ + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared-config.json"] + } + } +} +``` + +## #6 MOST COMMON MISTAKE: Creating Root Tasks + +**DO NOT create Root Tasks. ALWAYS create package tasks.** + +When you need to create a task (build, lint, test, typecheck, etc.): + +1. Add the script to **each relevant package's** `package.json` +2. Register the task in root `turbo.json` +3. Root `package.json` only contains `turbo run ` + +```json +// WRONG - DO NOT DO THIS +// Root package.json with task logic +{ + "scripts": { + "build": "cd apps/web && next build && cd ../api && tsc", + "lint": "eslint apps/ packages/", + "test": "vitest" + } +} + +// CORRECT - DO THIS +// apps/web/package.json +{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } } + +// apps/api/package.json +{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } + +// packages/ui/package.json +{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } } + +// Root package.json - ONLY delegates +{ "scripts": { "build": "turbo run build", "lint": "turbo run lint", "test": "turbo run test" } } + +// turbo.json - register tasks +{ + "tasks": { + "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, + "lint": {}, + "test": {} + } +} +``` + +**Why this matters:** + +- Package tasks run in **parallel** across all packages +- Each package's output is cached **individually** +- You can **filter** to specific packages: `turbo run test --filter=web` + +Root Tasks (`//#taskname`) defeat all these benefits. Only use them for tasks that truly cannot exist in any package (extremely rare). + +## #7 Tasks That Need Parallel Execution + Cache Invalidation + +Some tasks can run in parallel (don't need built output from dependencies) but must still invalidate cache when dependency source code changes. Using `dependsOn: ["^taskname"]` forces sequential execution. Using no dependencies breaks cache invalidation. + +**Use Transit Nodes for these tasks:** + +```json +// WRONG - forces sequential execution (SLOW) +"my-task": { + "dependsOn": ["^my-task"] +} + +// ALSO WRONG - no dependency awareness (INCORRECT CACHING) +"my-task": {} + +// CORRECT - use Transit Nodes for parallel + correct caching +{ + "tasks": { + "transit": { "dependsOn": ["^transit"] }, + "my-task": { "dependsOn": ["transit"] } + } +} +``` + +**Why Transit Nodes work:** + +- `transit` creates dependency relationships without matching any actual script +- Tasks that depend on `transit` gain dependency awareness +- Since `transit` completes instantly (no script), tasks run in parallel +- Cache correctly invalidates when dependency source code changes + +**How to identify tasks that need this pattern:** Look for tasks that read source files from dependencies but don't need their build outputs. + +## Missing outputs for File-Producing Tasks + +**Before flagging missing `outputs`, check what the task actually produces:** + +1. Read the package's script (e.g., `"build": "tsc"`, `"test": "vitest"`) +2. Determine if it writes files to disk or only outputs to stdout +3. Only flag if the task produces files that should be cached + +```json +// WRONG - build produces files but they're not cached +"build": { + "dependsOn": ["^build"] +} + +// CORRECT - outputs are cached +"build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] +} +``` + +No `outputs` key is fine for stdout-only tasks. For file-producing tasks, missing `outputs` means Turbo has nothing to cache. + +## Forgetting ^ in dependsOn + +```json +// WRONG - looks for "build" in SAME package (infinite loop or missing) +"build": { + "dependsOn": ["build"] +} + +// CORRECT - runs dependencies' build first +"build": { + "dependsOn": ["^build"] +} +``` + +The `^` means "in dependency packages", not "in this package". + +## Missing persistent on Dev Tasks + +```json +// WRONG - dependent tasks hang waiting for dev to "finish" +"dev": { + "cache": false +} + +// CORRECT +"dev": { + "cache": false, + "persistent": true +} +``` + +## Package Config Missing extends + +```json +// WRONG - packages/web/turbo.json +{ + "tasks": { + "build": { "outputs": [".next/**"] } + } +} + +// CORRECT +{ + "extends": ["//"], + "tasks": { + "build": { "outputs": [".next/**"] } + } +} +``` + +Without `"extends": ["//"]`, Package Configurations are invalid. + +## Root Tasks Need Special Syntax + +To run a task defined only in root `package.json`: + +```bash +# WRONG +turbo run format + +# CORRECT +turbo run //#format +``` + +And in dependsOn: + +```json +"build": { + "dependsOn": ["//#codegen"] // Root package's codegen +} +``` + +## Overwriting Default Inputs + +```json +// WRONG - only watches test files, ignores source changes +"test": { + "inputs": ["tests/**"] +} + +// CORRECT - extends defaults, adds test files +"test": { + "inputs": ["$TURBO_DEFAULT$", "tests/**"] +} +``` + +Without `$TURBO_DEFAULT$`, you replace all default file watching. + +## Caching Tasks with Side Effects + +```json +// WRONG - deploy might be skipped on cache hit +"deploy": { + "dependsOn": ["build"] +} + +// CORRECT +"deploy": { + "dependsOn": ["build"], + "cache": false +} +``` + +Always disable cache for deploy, publish, or mutation tasks. diff --git a/.agents/skills/turborepo/references/configuration/tasks.md b/.agents/skills/turborepo/references/configuration/tasks.md new file mode 100644 index 0000000..0ccc7ac --- /dev/null +++ b/.agents/skills/turborepo/references/configuration/tasks.md @@ -0,0 +1,285 @@ +# Task Configuration Reference + +Full docs: https://turborepo.dev/docs/reference/configuration#tasks + +## dependsOn + +Controls task execution order. + +```json +{ + "tasks": { + "build": { + "dependsOn": [ + "^build", // Dependencies' build tasks first + "codegen", // Same package's codegen task first + "shared#build" // Specific package's build task + ] + } + } +} +``` + +| Syntax | Meaning | +| ---------- | ------------------------------------ | +| `^task` | Run `task` in all dependencies first | +| `task` | Run `task` in same package first | +| `pkg#task` | Run specific package's task first | + +The `^` prefix is crucial - without it, you're referencing the same package. + +### Transit Nodes for Parallel Tasks + +For tasks like `lint` and `check-types` that can run in parallel but need dependency-aware caching: + +```json +{ + "tasks": { + "transit": { "dependsOn": ["^transit"] }, + "lint": { "dependsOn": ["transit"] }, + "check-types": { "dependsOn": ["transit"] } + } +} +``` + +**DO NOT use `dependsOn: ["^lint"]`** - this forces sequential execution. +**DO NOT use `dependsOn: []`** - this breaks cache invalidation. + +The `transit` task creates dependency relationships without running anything (no matching script), so tasks run in parallel with correct caching. + +## outputs + +Glob patterns for files to cache. **If omitted, nothing is cached.** + +```json +{ + "tasks": { + "build": { + "outputs": ["dist/**", "build/**"] + } + } +} +``` + +**Framework examples:** + +```json +// Next.js +"outputs": [".next/**", "!.next/cache/**"] + +// Vite +"outputs": ["dist/**"] + +// TypeScript (tsc) +"outputs": ["dist/**", "*.tsbuildinfo"] + +// No file outputs (lint, typecheck) +"outputs": [] +``` + +Use `!` prefix to exclude patterns from caching. + +## inputs + +Files considered when calculating task hash. Defaults to all tracked files in package. + +```json +{ + "tasks": { + "test": { + "inputs": ["src/**", "tests/**", "vitest.config.ts"] + } + } +} +``` + +**Special values:** + +| Value | Meaning | +| --------------------- | --------------------------------------- | +| `$TURBO_DEFAULT$` | Include default inputs, then add/remove | +| `$TURBO_ROOT$/` | Reference files from repo root | + +```json +{ + "tasks": { + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + "!README.md", + "$TURBO_ROOT$/tsconfig.base.json" + ] + } + } +} +``` + +## env + +Environment variables to include in task hash. + +```json +{ + "tasks": { + "build": { + "env": [ + "API_URL", + "NEXT_PUBLIC_*", // Wildcard matching + "!DEBUG" // Exclude from hash + ] + } + } +} +``` + +Variables listed here affect cache hits - changing the value invalidates cache. + +## cache + +Enable/disable caching for a task. Default: `true`. + +```json +{ + "tasks": { + "dev": { "cache": false }, + "deploy": { "cache": false } + } +} +``` + +Disable for: dev servers, deploy commands, tasks with side effects. + +## persistent + +Mark long-running tasks that don't exit. Default: `false`. + +```json +{ + "tasks": { + "dev": { + "cache": false, + "persistent": true + } + } +} +``` + +Required for dev servers - without it, dependent tasks wait forever. + +## interactive + +Allow task to receive stdin input. Default: `false`. + +```json +{ + "tasks": { + "login": { + "cache": false, + "interactive": true + } + } +} +``` + +## outputLogs + +Control when logs are shown. Options: `full`, `hash-only`, `new-only`, `errors-only`, `none`. + +```json +{ + "tasks": { + "build": { + "outputLogs": "new-only" // Only show logs on cache miss + } + } +} +``` + +## with + +Run tasks alongside this task. For long-running tasks that need runtime dependencies. + +```json +{ + "tasks": { + "dev": { + "with": ["api#dev"], + "persistent": true, + "cache": false + } + } +} +``` + +Unlike `dependsOn`, `with` runs tasks concurrently (not sequentially). Use for dev servers that need other services running. + +## interruptible + +Allow `turbo watch` to restart the task on changes. Default: `false`. + +```json +{ + "tasks": { + "dev": { + "persistent": true, + "interruptible": true, + "cache": false + } + } +} +``` + +Use for dev servers that don't automatically detect dependency changes. + +## description (Pre-release) + +Human-readable description of the task. + +```json +{ + "tasks": { + "build": { + "description": "Compiles the application for production deployment" + } + } +} +``` + +For documentation only - doesn't affect execution or caching. + +## passThroughEnv + +Environment variables available at runtime but NOT included in cache hash. + +```json +{ + "tasks": { + "build": { + "passThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"] + } + } +} +``` + +**Warning**: Changes to these vars won't cause cache misses. Use `env` if changes should invalidate cache. + +## extends (Package Configuration only) + +Control task inheritance in Package Configurations. + +```json +// packages/ui/turbo.json +{ + "extends": ["//"], + "tasks": { + "lint": { + "extends": false // Exclude from this package + } + } +} +``` + +| Value | Behavior | +| ---------------- | -------------------------------------------------------------- | +| `true` (default) | Inherit from root turbo.json | +| `false` | Exclude task from package, or define fresh without inheritance | diff --git a/.agents/skills/turborepo/references/environment/gotchas.md b/.agents/skills/turborepo/references/environment/gotchas.md new file mode 100644 index 0000000..eff77a4 --- /dev/null +++ b/.agents/skills/turborepo/references/environment/gotchas.md @@ -0,0 +1,145 @@ +# Environment Variable Gotchas + +Common mistakes and how to fix them. + +## .env Files Must Be in `inputs` + +Turbo does NOT read `.env` files. Your framework (Next.js, Vite, etc.) or `dotenv` loads them. But Turbo needs to know when they change. + +**Wrong:** + +```json +{ + "tasks": { + "build": { + "env": ["DATABASE_URL"] + } + } +} +``` + +**Right:** + +```json +{ + "tasks": { + "build": { + "env": ["DATABASE_URL"], + "inputs": ["$TURBO_DEFAULT$", ".env", ".env.local", ".env.production"] + } + } +} +``` + +## Strict Mode Filters CI Variables + +In strict mode, CI provider variables (GITHUB_TOKEN, GITLAB_CI, etc.) are filtered unless explicitly listed. + +**Symptom:** Task fails with "authentication required" or "permission denied" in CI. + +**Solution:** + +```json +{ + "globalPassThroughEnv": ["GITHUB_TOKEN", "GITLAB_CI", "CI"] +} +``` + +## passThroughEnv Doesn't Affect Hash + +Variables in `passThroughEnv` are available at runtime but changes WON'T trigger rebuilds. + +**Dangerous example:** + +```json +{ + "tasks": { + "build": { + "passThroughEnv": ["API_URL"] + } + } +} +``` + +If `API_URL` changes from staging to production, Turbo may serve a cached build pointing to the wrong API. + +**Use passThroughEnv only for:** + +- Auth tokens that don't affect output (SENTRY_AUTH_TOKEN) +- CI metadata (GITHUB_RUN_ID) +- Variables consumed after build (deploy credentials) + +## Runtime-Created Variables Are Invisible + +Turbo captures env vars at startup. Variables created during execution aren't seen. + +**Won't work:** + +```bash +# In package.json scripts +"build": "export API_URL=$COMPUTED_VALUE && next build" +``` + +**Solution:** Set vars before invoking turbo: + +```bash +API_URL=$COMPUTED_VALUE turbo run build +``` + +## Different .env Files for Different Environments + +If you use `.env.development` and `.env.production`, both should be in inputs. + +```json +{ + "tasks": { + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + ".env", + ".env.local", + ".env.development", + ".env.development.local", + ".env.production", + ".env.production.local" + ] + } + } +} +``` + +## Complete Next.js Example + +```json +{ + "$schema": "https://turborepo.dev/schema.v2.json", + "globalEnv": ["CI", "NODE_ENV", "VERCEL"], + "globalPassThroughEnv": ["GITHUB_TOKEN", "VERCEL_URL"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "env": [ + "DATABASE_URL", + "NEXT_PUBLIC_*", + "!NEXT_PUBLIC_ANALYTICS_ID" + ], + "passThroughEnv": ["SENTRY_AUTH_TOKEN"], + "inputs": [ + "$TURBO_DEFAULT$", + ".env", + ".env.local", + ".env.production", + ".env.production.local" + ], + "outputs": [".next/**", "!.next/cache/**"] + } + } +} +``` + +This config: + +- Hashes DATABASE*URL and NEXT_PUBLIC*\* vars (except analytics) +- Passes through SENTRY_AUTH_TOKEN without hashing +- Includes all .env file variants in the hash +- Makes CI tokens available globally diff --git a/.agents/skills/turborepo/references/environment/modes.md b/.agents/skills/turborepo/references/environment/modes.md new file mode 100644 index 0000000..eebbc2c --- /dev/null +++ b/.agents/skills/turborepo/references/environment/modes.md @@ -0,0 +1,101 @@ +# Environment Modes + +Turborepo supports different modes for handling environment variables during task execution. + +## Strict Mode (Default) + +Only explicitly configured variables are available to tasks. + +**Behavior:** + +- Tasks only see vars listed in `env`, `globalEnv`, `passThroughEnv`, or `globalPassThroughEnv` +- Unlisted vars are filtered out +- Tasks fail if they require unlisted variables + +**Benefits:** + +- Guarantees cache correctness +- Prevents accidental dependencies on system vars +- Reproducible builds across machines + +```bash +# Explicit (though it's the default) +turbo run build --env-mode=strict +``` + +## Loose Mode + +All system environment variables are available to tasks. + +```bash +turbo run build --env-mode=loose +``` + +**Behavior:** + +- Every system env var is passed through +- Only vars in `env`/`globalEnv` affect the hash +- Other vars are available but NOT hashed + +**Risks:** + +- Cache may restore incorrect results if unhashed vars changed +- "Works on my machine" bugs +- CI vs local environment mismatches + +**Use case:** Migrating legacy projects or debugging strict mode issues. + +## Framework Inference (Automatic) + +Turborepo automatically detects frameworks and includes their conventional env vars. + +### Inferred Variables by Framework + +| Framework | Pattern | +| ---------------- | ------------------- | +| Next.js | `NEXT_PUBLIC_*` | +| Vite | `VITE_*` | +| Create React App | `REACT_APP_*` | +| Gatsby | `GATSBY_*` | +| Nuxt | `NUXT_*`, `NITRO_*` | +| Expo | `EXPO_PUBLIC_*` | +| Astro | `PUBLIC_*` | +| SvelteKit | `PUBLIC_*` | +| Remix | `REMIX_*` | +| Redwood | `REDWOOD_ENV_*` | +| Sanity | `SANITY_STUDIO_*` | +| Solid | `VITE_*` | + +### Disabling Framework Inference + +Globally via CLI: + +```bash +turbo run build --framework-inference=false +``` + +Or exclude specific patterns in config: + +```json +{ + "tasks": { + "build": { + "env": ["!NEXT_PUBLIC_*"] + } + } +} +``` + +### Why Disable? + +- You want explicit control over all env vars +- Framework vars shouldn't bust the cache (e.g., analytics IDs) +- Debugging unexpected cache misses + +## Checking Environment Mode + +Use `--dry-run` to see which vars affect each task: + +```bash +turbo run build --dry-run=json | jq '.tasks[].environmentVariables' +``` diff --git a/.agents/skills/turborepo/references/filtering/patterns.md b/.agents/skills/turborepo/references/filtering/patterns.md new file mode 100644 index 0000000..17b9f1c --- /dev/null +++ b/.agents/skills/turborepo/references/filtering/patterns.md @@ -0,0 +1,152 @@ +# Common Filter Patterns + +Practical examples for typical monorepo scenarios. + +## Single Package + +Run task in one package: + +```bash +turbo run build --filter=web +turbo run test --filter=@acme/api +``` + +## Package with Dependencies + +Build a package and everything it depends on: + +```bash +turbo run build --filter=web... +``` + +Useful for: ensuring all dependencies are built before the target. + +## Package Dependents + +Run in all packages that depend on a library: + +```bash +turbo run test --filter=...ui +``` + +Useful for: testing consumers after changing a shared package. + +## Dependents Only (Exclude Target) + +Test packages that depend on ui, but not ui itself: + +```bash +turbo run test --filter=...^ui +``` + +## Changed Packages + +Run only in packages with file changes since last commit: + +```bash +turbo run lint --filter=[HEAD^1] +``` + +Since a specific branch point: + +```bash +turbo run lint --filter=[main...HEAD] +``` + +## Changed + Dependents (PR Builds) + +Run in changed packages AND packages that depend on them: + +```bash +turbo run build test --filter=...[HEAD^1] +``` + +Or use the shortcut: + +```bash +turbo run build test --affected +``` + +## Directory-Based + +Run in all apps: + +```bash +turbo run build --filter=./apps/* +``` + +Run in specific directories: + +```bash +turbo run build --filter=./apps/web --filter=./apps/api +``` + +## Scope-Based + +Run in all packages under a scope: + +```bash +turbo run build --filter=@acme/* +``` + +## Exclusions + +Run in all apps except admin: + +```bash +turbo run build --filter=./apps/* --filter=!admin +``` + +Run everywhere except specific packages: + +```bash +turbo run lint --filter=!legacy-app --filter=!deprecated-pkg +``` + +## Complex Combinations + +Apps that changed, plus their dependents: + +```bash +turbo run build --filter=...[HEAD^1] --filter=./apps/* +``` + +All packages except docs, but only if changed: + +```bash +turbo run build --filter=[main...HEAD] --filter=!docs +``` + +## Debugging Filters + +Use `--dry` to see what would run without executing: + +```bash +turbo run build --filter=web... --dry +``` + +Use `--dry=json` for machine-readable output: + +```bash +turbo run build --filter=...[HEAD^1] --dry=json +``` + +## CI/CD Patterns + +PR validation (most common): + +```bash +turbo run build test lint --affected +``` + +Deploy only changed apps: + +```bash +turbo run deploy --filter=./apps/* --filter=[main...HEAD] +``` + +Full rebuild of specific app and deps: + +```bash +turbo run build --filter=production-app... +``` diff --git a/.agents/skills/unit-tests/SKILL.md b/.agents/skills/unit-tests/SKILL.md new file mode 100644 index 0000000..6afd80d --- /dev/null +++ b/.agents/skills/unit-tests/SKILL.md @@ -0,0 +1,111 @@ +--- +name: unit-tests +description: | + Guardrails for adding unit tests in bklit-ui without over-testing. Use when the + user mentions unit test, unit tests, tests, test coverage, add tests, write tests, + vitest, jest, or asks whether something should be tested. +--- + +# Unit Tests (bklit-ui) + +Read this skill before proposing or writing tests. **Default stance: fewer, higher-signal tests.** + +--- + +## When to add tests + +Add tests when they **lock in behavior that is easy to break silently**: + +| Worth testing | Why | +|---------------|-----| +| Pure functions / modules | Stable inputs → outputs; fast; no DOM | +| Formatters, parsers, scale math, bounds | Regression on string/number output is user-visible | +| Codegen / export / registry helpers | Output shape is the contract | +| Non-obvious edge cases | Empty data, reversed ranges, clamping | + +**Examples in this repo:** `chart-formatters.test.ts`, `highlight-segment-bounds.test.ts`, `animation.test.ts`, `apps/web/lib/studio/__tests__/*`. + +--- + +## When NOT to add tests (unless explicitly requested) + +Do **not** add tests just to increase coverage or “be thorough”: + +| Skip | Why | +|------|-----| +| React component render smoke tests | visx, motion, portals → brittle; manual/docs check is cheaper | +| `memo()` / guard extractions (#65-style) | Structural perf refactors; output unchanged; RTL mount assertions are heavy | +| Default prop passthrough | `formatValue = intFmt` — types + formatter tests cover it | +| Third-party library behavior | Don’t re-test d3, visx, or React | +| Trivial getters / one-line wrappers | No regression signal | +| Snapshot entire chart SVG/JSX | High churn, low signal | + +If the user asks “should we test X?” — **say no** when X falls in this table, and suggest a lighter alternative (pure helper test, manual check, CI build). + +--- + +## Repo conventions + +**Runner:** Node built-in test runner + `tsx` (not Jest/Vitest unless the repo adopts them later). + +```bash +pnpm test # root — turbo runs packages with a test script +pnpm --filter @bklitui/ui test +cd apps/web && pnpm test +``` + +**Place tests:** `**/__tests__/**/*.test.ts` next to the code under test. + +**Pattern:** + +```ts +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { myFn } from "../my-module"; + +describe("myFn", () => { + it("handles empty input", () => { + assert.equal(myFn([]), expected); + }); +}); +``` + +**Equivalence tests** (preferred for formatters): assert shared module output matches the **previous inline** call (e.g. `toLocaleDateString` with same locale/options) so tests stay timezone-safe and prove no visual regression. + +**New package test script:** add to `package.json`: + +```json +"test": "node scripts/run-tests.mjs" +``` + +Use a small `scripts/run-tests.mjs` that collects `*.test.ts` from `__tests__` and invokes `node --import tsx --test` — shell globs break on Linux CI. Add `tsx` as a devDependency if missing. Wire into root `turbo.json` `test` task; CI runs `pnpm test`. + +--- + +## Decision checklist (run before writing) + +1. Is the logic **pure** or extractable to pure functions? → Test that. Consider extracting first. +2. Would a test fail on a **real user-visible bug**? → Good candidate. +3. Does it need **jsdom / RTL / Playwright**? → Stop; justify to user or defer to manual/visual check. +4. Did the user **ask** for tests? → Still apply this skill; don’t over-deliver. +5. How many cases? → **3–10 focused cases**, not exhaustive matrices. + +--- + +## What to tell the user + +When recommending tests, be explicit: + +- **Add:** “Test `chart-formatters.ts` — pure, high regression value.” +- **Skip:** “Skip component tests for the memo split — no output change; build + manual chart docs are enough.” +- **CI:** Mention `pnpm test` in PR test plan when adding or changing tests. + +--- + +## Anti-patterns + +- Adding Jest/Vitest/Testing Library for a one-off without team buy-in +- Mocking entire chart context to assert `useMemo` call counts +- Testing implementation details (hook order, internal component names) +- Duplicating type-checker work (`expect(typeof x).toBe('function')`) +- Committing tests that only pass locally due to hard-coded timezone/locale strings diff --git a/.agents/skills/wiki-llms-txt/SKILL.md b/.agents/skills/wiki-llms-txt/SKILL.md new file mode 100644 index 0000000..a03652d --- /dev/null +++ b/.agents/skills/wiki-llms-txt/SKILL.md @@ -0,0 +1,129 @@ +--- +name: wiki-llms-txt +description: Generates llms.txt and llms-full.txt files for LLM-friendly project documentation following the llms.txt specification. Use when the user wants to create LLM-readable summaries, llms.txt files, or make their wiki accessible to language models. +license: MIT +metadata: + author: Microsoft + version: "1.0.0" +--- + +# llms.txt Generator + +Generate `llms.txt` and `llms-full.txt` files that provide LLM-friendly access to wiki documentation, following the [llms.txt specification](https://llmstxt.org/). + +## When This Skill Activates + +- User asks to generate `llms.txt` or mentions the llms.txt standard +- User wants to make documentation "LLM-friendly" or "LLM-readable" +- User asks for a project summary file for language models +- User mentions `llms-full.txt` or context-expanded documentation + +## Source Repository Resolution (MUST DO FIRST) + +Before generating, resolve the source repository context: + +1. **Check for git remote**: Run `git remote get-url origin` +2. **Ask the user**: _"Is this a local-only repository, or do you have a source repository URL?"_ + - Remote URL → store as `REPO_URL` + - Local → use relative paths only +3. **Determine default branch**: Run `git rev-parse --abbrev-ref HEAD` +4. **Do NOT proceed** until resolved + +## llms.txt Format (Spec-Compliant) + +The file follows the [llms.txt specification](https://llmstxt.org/): + +```markdown +# {Project Name} + +> {Dense one-paragraph summary — what it does, who it's for, key technologies} + +{Important context paragraphs — constraints, architectural philosophy, non-obvious things} + +## {Section Name} + +- [{Page Title}]({relative-path-to-md}): {One-sentence description of what the reader will learn} + +## Optional + +- [{Page Title}]({relative-path-to-md}): {Description — these can be skipped for shorter context} +``` + +### Key Rules + +1. **H1** — Project name (exactly one, required) +2. **Blockquote** — Dense, specific summary (required). Must be unique to THIS project. +3. **Context paragraphs** — Non-obvious constraints, things LLMs would get wrong without being told +4. **H2 sections** — Organized by topic, each with a list of `[Title](url): Description` entries +5. **"Optional" H2** — Special meaning: links here can be skipped for shorter context +6. **Relative links** — All paths relative to wiki directory +7. **Dynamic** — ALL content derived from actual wiki pages, not templates +8. **Section order** — Most important first: Onboarding → Architecture → Getting Started → Deep Dive → Optional + +### Description Quality + +| ❌ Bad | ✅ Good | +|--------|---------| +| "Architecture overview" | "System architecture showing how Orleans grains communicate via message passing with at-least-once delivery" | +| "Getting started guide" | "Prerequisites, local dev setup with Docker Compose, and first API call walkthrough" | +| "The API reference" | "REST endpoints with auth requirements, rate limits, and request/response schemas" | + +## llms-full.txt Format + +Same structure as `llms.txt` but with full content inlined: + +```markdown +# {Project Name} + +> {Same summary} + +{Same context} + +## {Section Name} + + +{Full markdown content — frontmatter stripped, citations and diagrams preserved} + +``` + +### Inlining Rules + +- **Strip YAML frontmatter** (`---` blocks) from each page +- **Preserve Mermaid diagrams** — keep `` ```mermaid `` fences intact +- **Preserve citations** — all `[file:line](URL)` links stay as-is +- **Preserve tables** — all markdown tables stay intact +- **Preserve `` comments** — these provide diagram provenance + +## Prerequisites + +This skill works best when wiki pages already exist (via `/deep-wiki:generate` or `/deep-wiki:page`). If no wiki exists yet: + +1. Suggest running `/deep-wiki:generate` first +2. OR generate a minimal `llms.txt` from README + source code scan (without wiki page links) + +## Output Files + +Generate three files: + +| File | Purpose | Discoverability | +|------|---------|-----------------| +| `./llms.txt` | Root discovery file | Standard path per llms.txt spec. GitHub MCP `get_file_contents` and `search_code` find this first. | +| `wiki/llms.txt` | Wiki-relative links | For VitePress deployment and wiki-internal navigation. | +| `wiki/llms-full.txt` | Full inlined content | Comprehensive reference for agents needing all docs in one file. | + +The root `./llms.txt` links into `wiki/` (e.g., `[Guide](./wiki/onboarding/contributor-guide.md)`). The `wiki/llms.txt` uses wiki-relative paths (e.g., `[Guide](./onboarding/contributor-guide.md)`). + +If a root `llms.txt` already exists and was NOT generated by deep-wiki, do NOT overwrite it. + +## Validation Checklist + +Before finalizing: + +- [ ] All linked files in `llms.txt` actually exist +- [ ] All `` blocks in `llms-full.txt` have real content (not empty) +- [ ] Blockquote is specific to this project (not generic boilerplate) +- [ ] Sections ordered by importance +- [ ] No duplicate page entries across sections +- [ ] "Optional" section only contains truly optional content +- [ ] `llms.txt` is concise (1-5 KB) +- [ ] `llms-full.txt` contains all wiki pages \ No newline at end of file diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..0141646 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "quant", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["dev"], + "port": 5173 + }, + { + "name": "quant-build", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["preview", "--port", "4173"], + "port": 4173 + } + ] +} diff --git a/.claude/skills/add-x-tweet b/.claude/skills/add-x-tweet new file mode 120000 index 0000000..09045bf --- /dev/null +++ b/.claude/skills/add-x-tweet @@ -0,0 +1 @@ +../../.agents/skills/add-x-tweet \ No newline at end of file diff --git a/.claude/skills/bklit-playground b/.claude/skills/bklit-playground new file mode 120000 index 0000000..775a288 --- /dev/null +++ b/.claude/skills/bklit-playground @@ -0,0 +1 @@ +../../.agents/skills/bklit-playground \ No newline at end of file diff --git a/.claude/skills/bklit-ship b/.claude/skills/bklit-ship new file mode 120000 index 0000000..b2c4b99 --- /dev/null +++ b/.claude/skills/bklit-ship @@ -0,0 +1 @@ +../../.agents/skills/bklit-ship \ No newline at end of file diff --git a/.claude/skills/bklit-studio b/.claude/skills/bklit-studio new file mode 120000 index 0000000..71511a7 --- /dev/null +++ b/.claude/skills/bklit-studio @@ -0,0 +1 @@ +../../.agents/skills/bklit-studio \ No newline at end of file diff --git a/.claude/skills/bklit-studio-chart-performance b/.claude/skills/bklit-studio-chart-performance new file mode 120000 index 0000000..f648c07 --- /dev/null +++ b/.claude/skills/bklit-studio-chart-performance @@ -0,0 +1 @@ +../../.agents/skills/bklit-studio-chart-performance \ No newline at end of file diff --git a/.claude/skills/bklit-ui b/.claude/skills/bklit-ui new file mode 120000 index 0000000..60b1881 --- /dev/null +++ b/.claude/skills/bklit-ui @@ -0,0 +1 @@ +../../.agents/skills/bklit-ui \ No newline at end of file diff --git a/.claude/skills/pr-open b/.claude/skills/pr-open new file mode 120000 index 0000000..d38a5ab --- /dev/null +++ b/.claude/skills/pr-open @@ -0,0 +1 @@ +../../.agents/skills/pr-open \ No newline at end of file diff --git a/.claude/skills/turborepo b/.claude/skills/turborepo new file mode 120000 index 0000000..b69a7d9 --- /dev/null +++ b/.claude/skills/turborepo @@ -0,0 +1 @@ +../../.agents/skills/turborepo \ No newline at end of file diff --git a/.claude/skills/unit-tests b/.claude/skills/unit-tests new file mode 120000 index 0000000..fd09d5b --- /dev/null +++ b/.claude/skills/unit-tests @@ -0,0 +1 @@ +../../.agents/skills/unit-tests \ No newline at end of file diff --git a/.claude/skills/wiki-llms-txt b/.claude/skills/wiki-llms-txt new file mode 120000 index 0000000..fb7a005 --- /dev/null +++ b/.claude/skills/wiki-llms-txt @@ -0,0 +1 @@ +../../.agents/skills/wiki-llms-txt \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..df4e0b1 --- /dev/null +++ b/.env @@ -0,0 +1,10 @@ +# Deploy config (Forgejo Actions). This file IS committed — it's config, not secrets. +# Secrets go via Forgejo → repo → Settings → Actions → Secrets (+ RUNTIME_KEYS). +# Push to main → platform builds the Vite app with bun and serves dist/ on the domain (SSL auto). + +# Site domain. Point its A-record at the platform IP (154.83.149.72) so SSL can issue. +DOMAIN=agentsquant.xyz + +# Static front-end (Vite + React → dist). Default, but pinned explicitly. +DEPLOY=static +BUILD_DIR=dist diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b15c7db --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Copy to .env.local and fill in. + +# FactsAI deep-research API key (https://factsai.org/docs). +# WARNING: with a static build this is baked into the client JS and visible to anyone. +# For production, leave KEY empty and set PROXY to a tiny serverless function that injects +# the key server-side and adds CORS headers — that hides the key and avoids browser CORS. +VITE_FACTSAI_KEY= +VITE_FACTSAI_PROXY= + +# ── Paper-trading worker (worker/, run with `pnpm worker`) ────────────────────── +# All optional — the worker runs in dry-run paper mode with sane defaults if unset. +# Telegram: create a bot via @BotFather for the token; get your chat id from @userinfobot. +# Leave both empty to run head-less (signals/fills print to stdout instead). +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= +# Universe + cadence +QUANT_COINS=BTC,ETH,SOL +QUANT_INTERVAL=1h +QUANT_POLL_MS=60000 +# Paper risk model +QUANT_EQUITY_START=10000 +QUANT_LEVERAGE=5 +QUANT_CONF_FLOOR=0.55 diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..e385e35 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,22 @@ +name: deploy +on: + push: + branches: [main] + workflow_dispatch: # lets the admin panel trigger a redeploy (Enable) +jobs: + deploy: + runs-on: docker-cli + container: + image: platform-ci-base:latest + volumes: + - /srv/sites:/srv/sites + - /srv/platform/caddy/sites:/srv/platform/caddy/sites + steps: + - uses: https://git.154.83.149.72.nip.io/platform/ci-templates@main + with: + repo: ${{ github.repository }} + ref: ${{ github.ref_name }} + sha: ${{ github.sha }} + token: ${{ github.token }} + secrets_json: ${{ toJSON(secrets) }} + vars_json: ${{ toJSON(vars) }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa9271a --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Worker paper-trading state (regenerated at runtime) +worker/data + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.interface-design/system.md b/.interface-design/system.md new file mode 100644 index 0000000..0396c74 --- /dev/null +++ b/.interface-design/system.md @@ -0,0 +1,90 @@ +# $QUANT Terminal — interface design system + +**Direction:** "Carbon Desk" — a serious institutional quant trading terminal (Linear × +Bloomberg). Dark, dense, calm. The product is the live decision pipeline; the **agent +committee → verdict** is the hero. Reading order everywhere: **CALL → WHY → EVIDENCE**. + +**Feel:** precise, quiet, instrument-like. No marketing voice. Craft whispers. + +## Depth strategy + +**Borders + surface lightness — NO shadows.** Elevation by lightness: +- page `--color-paper` #0B0C0E → panel chrome `--color-panel` #111316 → raised/hover/active + `--color-panel-2` #181B20. Data cells & wells **recede** to `--color-inset` #0E0F12. +- Borders are light-on-dark rgba: `line` .10 (standard), `line-soft` .06 (dividers), + `line-strong` .16 (emphasis / zero-axis). `line-amber` .45 only for the verdict accent. +- Bar/ring tracks use `--color-track` rgba(255,255,255,.09) — a carved channel, never `line-soft`. + +## Color + +- **One accent: amber `--color-accent` #FFB020** — reserved for "the call" (verdict accent + strip, L4 pipeline dot, active tab, PAPER chip). Do not spread it. +- **Semantic only:** long `#2BD974`, short `#F2444D` (+ `-dim` 14% fills). Never decorative. +- Direction must never be color-only — pair with a glyph/sign (▲ ▼ —, +/−). + +## Text hierarchy (all AA on panel) + +`--color-bone` #E8EAED primary · `--color-bone-dim` #AAB1BA secondary/prose · +`--color-muted` #828A94 labels · `--color-faint` #727983 captions/ticks. +Rule: value=bone, supporting prose=bone-dim, labels=muted, codes/units/ticks=faint. + +## Type + +- Display/UI **Geist**, data **Geist Mono**. All numbers mono + `.tnum` (tabular). +- Scale tokens: `--text-micro` 10px · `--text-label` 11px · `--text-read` 12px · `--text-data` 13px. + Big display via inline `clamp()` (verdict glyph clamp(3rem,6vw,5rem)). +- Labels: `.label` (11px, .14em) and `.label-sm` (10px, .1em) are the ONLY label recipes. + Two letter-spacings, not eight. +- Weight: bold (700) reserved for display (verdict glyph, masthead, QUANT). Data = 500/medium. + +## Spacing & radius + +- 4px base. Panel inset = `px-4 py-3`. Hairline grids via `gap-px` over `bg-line-soft`. +- Radius scale 2/4/6/10. Panels `rounded-md` + `overflow-hidden`; nested wells clip to parent. + +## Key patterns + +- **Panel** — `rounded-md border border-line bg-panel overflow-hidden`; header = `.label` + + faint meta. Accent panel adds a 2px amber top strip as a child div (clean rounded corners). +- **Cell** — recessed `bg-inset`, `.label-sm` over `.tnum` mono value; optional `sub` (agent tag) + and a thin `--color-track` bar. +- **DivergeBar** — the signature: shared zero-axis; LONG fills right (green), SHORT left (red), + SKIP a centred muted tick. Conviction = length. Committee ends in a NET LEAN row whose amber + marker is the aggregate — visually ties votes → verdict. +- **VerdictHero** — full-width band, the hero: clamp() decision glyph in dir color + arrow, + quorum ticks, big ConfidenceRing, then size/TP/SL/RR cells. Amber accent strip. +- **Pipeline** — honest L1→L6 with status dots (live=green, neutral=muted, the call=amber on L4, + off-terminal=faint). Never overclaim what the page doesn't do. + +## Terminal layout (workspace) + +The terminal is a **multi-pane workspace**, not tabs — everything visible at once like a real desk. +Order: status bar → masthead + coin tabs → reading guide → **VERDICT band on top** (the call, +full width) → 3-column grid `lg:grid-cols-[280px_minmax(0,1fr)_360px]` (stacks to 1 col below lg): +- **Left** — `PipelineRail` (vertical L1→L6) + Market + Indicators stat cells (the "numbers"). +- **Center** — the candlestick chart (the dominant pane). +- **Right** — `AgentGraph` (signature) + Committee analytics list. + +`AgentGraph` (`components/viz/AgentGraph.tsx`) is hub-and-spoke: 4 agent nodes feed one central +verdict node; edge colour = vote (long/short/skip), edge weight + node fill = conviction; the +centre shows decision + confidence. Honest (exactly the 4 real agents), animated between states. + +## Charts + +Charts come from **bklit-ui** (shadcn registry `@bklit`, built on @visx + motion), installed as +source under `src/components/charts/`. The price panel uses the **candlestick-chart** +(`src/components/viz/PriceCandles.tsx`) fed by real Hyperliquid OHLC, themed to Carbon Desk: +`positiveFill=var(--color-long)`, `negativeFill=var(--color-short)`. Dark chart vars are +activated via `class="dark"` on ``. Add more charts with +`npx shadcn@latest add @bklit/`. + +Two gotchas (already handled): (1) the alpha `@visx ParentSize` fails to size under React 19 — +`candlestick-chart.tsx` was patched to use a `useElementSize` ResizeObserver hook instead. +(2) Keep the global `* { min-width: 0 }` reset inside `@layer base`, or it (unlayered) beats +Tailwind's `min-w-*` utilities and breaks `flex-wrap` layouts. + +## States + +Seeded-vs-live is flagged (verdict shows a "seed · awaiting live" chip when `live===false`); +StatusBar surfaces stale/error (`DATA STALE` when fetch errors or `ago>40s`); footer copy +degrades the "live" claim accordingly. Global amber `:focus-visible` ring on all interactives. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea47308 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# $QUANT — landing + live terminal + +An orchestra of ~18 AI agents trading Hyperliquid perps. Marketing site (LARP) with a +**Living Agent Swarm** aesthetic, driven by **real Hyperliquid market data**. + +- **Landing** (`/`) — hero swarm + 6-layer pipeline + live signal desk + agent roster + + performance + tokenomics + roadmap + Telegram modes + FAQ + waitlist. +- **Terminal** (`/terminal`) — live dashboard: real price/funding/OI, in-browser TA, agent + vote matrix and a LONG/SHORT/SKIP verdict. + +## Stack + +React 19 · Vite 8 · TypeScript · Tailwind v4 · shadcn-style UI · anime.js v4 (SVG) · +Recharts · React Router · Lenis. Package manager: **pnpm**. + +## Run + +```bash +pnpm install +pnpm dev # dev server (http://localhost:5173) +pnpm build # production build → dist/ (deploy this only) +pnpm preview # serve the built dist/ +pnpm gen:icons # regenerate favicon.ico / og.png from public/favicon.svg +``` + +## Data + +- **Live:** Hyperliquid public info API (`POST https://api.hyperliquid.xyz/info`, no auth, + CORS `*`) → live BTC/ETH/SOL price, funding, OI. Indicators (RSI/MACD/EMA/ATR/Bollinger) are + computed in-browser in `src/lib/ta.ts`; agent votes + verdict in `src/lib/signals.ts`. +- **Seed fallback:** `src/data/seedMarket.ts` renders deterministic data instantly (labelled + non-live) so cards never hang; live data replaces it. +- **Honesty:** anything that can't be real pre-TGE (token price/holders/PnL track record) is + rendered but clearly labelled **simulated**. + +## Live paper-trading worker + +`worker/` is an always-on Node loop that **reuses the browser brain** (`src/lib/ta.ts` ++ `signals.ts`) server-side: real candles → real TA → 4-agent vote → verdict, then it +**paper-trades** the verdict against live Hyperliquid prices and broadcasts every fill to +**Telegram**. This turns the simulated PnL track record into an honest, verifiable one — +no order is ever signed (`dry-run` only). See [`worker/README.md`](worker/README.md). + +```bash +pnpm worker # run forever (Telegram optional — prints to stdout if unset) +pnpm worker:once # single tick smoke test +``` + +## ⚠️ Fill these before launch + +Edit `src/data/site.ts`: +- `x` — the project's X/Twitter URL (manager-provided; shown in header + footer) +- `telegram` — Telegram link +- `contract` — $QUANT Solana mint (post-TGE) + +Other: +- **Wallet:** `src/components/ConnectWallet.tsx` is a cosmetic Solana connect modal (honest + pre-TGE). To wire real **Reown AppKit (Solana)**, add a `projectId` (Reown dashboard) and + swap the modal body — see the project plan. +- **DexScreener token strip:** add the `$QUANT` pair address once it lists. +- The `base-verification` meta tag is already in `index.html`. + +## Research agent (FactsAI) + +The landing has a **"Research agent · L4"** section that renders a real AI market brief with +cited web sources from [FactsAI](https://factsai.org/docs) (`src/lib/factsai.ts`, +`components/sections/AIBrief.tsx`). It self-hides when the feed is unreachable, so the page +never looks broken. + +⚠️ **A static site cannot call FactsAI directly** — the API returns no `Access-Control-Allow-Origin` +(browser blocks it) and a client key would be public. Also, at the time of building, FactsAI's +endpoint returned `500 "Service configuration error"` (their backend). To enable it: + +1. Deploy the included proxy: [`proxy/factsai-worker.js`](proxy/factsai-worker.js) (Cloudflare + Worker — holds the key + adds CORS). See the header comment for `wrangler` steps. +2. Set `VITE_FACTSAI_PROXY=https://.workers.dev` in `.env.local`, then `pnpm build`. +3. FactsAI must also fix their upstream `500` before real answers return. + +Without a proxy, set `VITE_FACTSAI_KEY` in `.env.local` for local dev only (the key is baked into +the bundle and CORS will still block production). + +## Deploy + +Ship **`dist/` only** to the GitLab static host (agency `templates/starter` pipeline). If the +app is served without SPA fallback, deep-linking `/terminal` on refresh may 404 — configure the +host to fall back to `index.html`, or switch to `HashRouter` in `src/App.tsx`. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..e47a7573ffa8e2e3264a2d7b7ba678634b0c5fc8 GIT binary patch literal 176919 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®YgRFz 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

xf( z0qDtTN^=y*twK4gx76z}0O%JA%S5}G?#@Ktoi(NG#dRt#M9wFxfX4Cj%X*2qLpnXv z+=lE%F##NfsKKeyrw^I{%z#up2PnV#j$Rwm=tP$VadN7+P&sPPt6qE$E11RK;7Rd; z)337B>p1n+I~4jYLTIGHYiPvhFJq!TqMmzNyZnDQU|tR#kNUw$rKSOXpNKT?e^iC) zi97I8GYRTcu$1*)bK+zqGa?XzLk*xubBqt$LzSHH-Z$`0%?ZKK+)&K+TGI$%H01

+8V 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 ema21` и `rsi > 50`; **SHORT**, если `ema9 < ema21` и `rsi < 50`; иначе **SKIP**. +- `score = clamp(|emaDiffPct| × 14 + |rsi − 50| / 60, 0, 1)`. +- Пример reason: `«EMA9 above EMA21, RSI 56»`. + +### BreakoutAgent — пробой полос Боллинджера +- **LONG**, если `%B > 0.92`; **SHORT**, если `%B < 0.08`; иначе **SKIP**. +- `score = clamp(|%B − 0.5| × 1.7, 0, 1)`. + +### MeanReversionAgent — возврат к среднему (фейд крайностей) +- **LONG**, если `rsi < 32` и `%B < 0.15`; **SHORT**, если `rsi > 68` и `%B > 0.85`; иначе **SKIP**. +- `score` растёт по мере удаления RSI от порога. + +### FundingAgent — фейд перекошенного фандинга +- **SHORT**, если `funding-Z > 1.5` (лонги перегреты); **LONG**, если `funding-Z < −1.5`; иначе **SKIP**. +- `score = clamp(|funding-Z| / 3, 0, 1)`. + +Для голосов SKIP score дополнительно занижается (×0.3–0.4) — пропуск тоже взвешивается. + +--- + +## 5. Слой L4 — агрегация и вердикт (`src/lib/signals.ts`, `aggregate()`) + +Голоса сводятся в итог `Verdict`: + +```ts +{ decision, // LONG / SHORT / SKIP + confidence, // 0..1 + sizePct, // рекомендуемый размер позиции, % + tpPct, slPct, // take-profit / stop-loss, % + reasoning, // текстовое объяснение + net } // чистый счёт голосов +``` + +**Алгоритм:** +1. `net = Σ (±1 × score)` по всем не-SKIP голосам (LONG = +, SHORT = −). +2. `agreement` = доля голосов, совпавших со знаком `net`. +3. Порог `thr = 0.5`: `net > 0.5 → LONG`, `net < −0.5 → SHORT`, иначе `SKIP`. +4. **Уверенность:** + - для сделки: `confidence = clamp(0.45 + |net|×0.18 + agreement×0.18, 0, 0.97)`; + - для SKIP: `clamp(0.3 + |net|×0.2, 0.18, 0.5)`. +5. **Параметры риска** (масштабируются от волатильности `vol = clamp(atrPct, 0.4, 6)`): + - `tpPct = clamp(vol × 1.6, 0.8, 6)`; + - `slPct = clamp(vol × 0.9, 0.6, 3.5)`; + - `sizePct = clamp(confidence × 3.2, 0.5, 3.2)`. +6. `reasoning` собирается из числа согласных агентов и причины лидирующего голоса, + например: `«1/1 agents align LONG. EMA9 above EMA21, RSI 56. RR 1.9»` (RR = TP/SL). + +> Примечание по терминологии: в UI этот слой назван «LLMScreener / ConfidenceGate», но в +> текущей реализации это **детерминированная агрегация формулами**, а не вызов LLM. +> Порог уверенности (роль «ConfidenceGate») в воркере вынесен в `confFloor` (см. §6). + +--- + +## 6. Слой L5 — paper-trading движок (`worker/paper.ts`) + +Превращает вердикт в **бумажную** позицию и ведёт её по реальной цене Hyperliquid. +Режим жёстко `dry-run`: ордера на биржу **не подписываются и не отправляются** — это +симуляция исполнения на живых рыночных данных, считающая реальный PnL. + +**Открытие — `maybeOpen(coin, price, verdict)`:** +- пропуск, если `decision = SKIP`, или `confidence < confFloor` (по умолчанию 0.55), + или по монете уже есть открытая позиция (`maxOpenPerCoin = 1`); +- `entry = price` (текущая mark-цена); +- `tp = entry × (1 ± tpPct/100)`, `sl = entry × (1 ∓ slPct/100)` (знак по направлению); +- `notional = equity × sizePct/100 × leverage` (по умолчанию leverage = 5); +- сохраняется `Position { id, coin, dir, entry, notional, tp, sl, tpPct, slPct, confidence, reason, openedAt }`. + +**Ведение — `markToMarket(coin, price)`:** на каждом тике, если цена пересекла TP или SL, +позиция закрывается по уровню (исполнение по TP/SL предполагается точным). + +**Закрытие — расчёт PnL:** +``` +move = (exit − entry) / entry +pnlPct = (dir = LONG ? move : −move) × 100 +pnlUsd = notional × pnlPct / 100 +equity = equity + pnlUsd // капитал компаундируется +``` +Закрытая сделка пишется в `Trade` (добавляются `exit, closedAt, pnlPct, pnlUsd, outcome +('tp'|'sl'), holdMin`). + +**Статистика — `stats()` (источник команды `/pnl`):** число сделок, открытые позиции, +винрейт, суммарный PnL ($), ROI (%), profit factor, средний выигрыш/проигрыш, лучшая и +худшая сделка, текущий капитал. + +--- + +## 7. Слой L6 — Telegram (`worker/telegram.ts`, `worker/loop.ts`) + +Реализован на «голом» Bot API (без библиотек), через `fetch`. + +**Исходящие — `tgSend()`:** `POST .../sendMessage` с `parse_mode: HTML`. Сообщения шлются +при **открытии** и **закрытии** позиции. Если токен/чат не заданы — текст печатается в +stdout (воркер работает и без Telegram). + +Пример сообщения об открытии (реальный формат): +``` +🟢 LONG BTC-PERP @ $62,135 +size 2.6% · ×5 → $1,300 notional +TP +1.30% ($62,943) · SL -0.70% ($61,700) +confidence 81% · 1/1 agents align LONG. EMA9 above EMA21, RSI 56. RR 1.9. +``` +Пример закрытия: +``` +✅ TP BTC-PERP LONG +$62,135 → $62,943 (+1.30%, +$16.90) +held 2.4h · equity $10,017 +``` + +**Входящие — `tgPoll()`:** long-polling `getUpdates` (offset-based), раз в ~3 c. +Поддерживаемые команды: `/status` (капитал, открытые позиции, винрейт), `/positions` +(детально по открытым), `/pnl` (он же `/learn` — сводная статистика по закрытым сделкам), +`/help`, `/start`. + +> Терминология: команда `/learn` в текущей реализации возвращает **статистическую сводку** +> по закрытым сделкам, а не дообучение модели. + +--- + +## 8. Воркер: рантайм (`worker/loop.ts`) + +Постоянный фоновый цикл — работает независимо от того, открыт сайт или нет. + +**Тик (`tick()`)** — по умолчанию раз в `pollMs = 60 000` мс: +1. один запрос `getMarketStats(coins)` на все монеты; +2. по каждой монете (`tickCoin`): + `getCandles` → `summarizeTA` → `screen` (голоса + вердикт) → `price = mark || ta.price` + → `markToMarket` (закрыть, если задело TP/SL, + уведомление) → `maybeOpen` (открыть, если + есть сигнал, + уведомление) → запись в лог сигналов **при смене решения** по монете; +3. `save()` — сохранение состояния на диск. + +Параллельно: `commandLoop()` раз в 3 c (если задан Telegram-токен). + +**Запуск:** +```bash +pnpm worker # бесконечный цикл +pnpm worker:once # один тик и выход (смоук-тест) +``` + +--- + +## 9. Хранение состояния (`worker/store.ts`) + +Один JSON-файл `worker/data/state.json` (в `.gitignore`), запись **атомарная** +(во временный файл + `rename`). Переживает перезапуск. Структура `State`: + +```ts +{ startedAt, mode, + equity, // текущий капитал + positions: Position[], // открытые + closed: Trade[], // закрытые (последние 500) + signals: SignalLog[], // лог решений при смене (последние 200) + equityCurve: { t, equity }[] } // кривая капитала (последние 1000 точек) +``` + +Это ровно тот формат, который будущий HTTP-эндпоинт (`/api/equity`, `/api/signals`) сможет +отдавать на сайт. (SQLite заменяема без изменения вызывающего кода — сейчас используется +JSON.) + +--- + +## 10. Фронтенд (`src/`) + +Две страницы (React Router, `src/App.tsx`): + +- **`/` — лендинг** (`src/pages/Landing.tsx`): секции Hero, Thesis, Layers (6-слойный + конвейер), SignalDesk, AIBrief (ресёрч-агент), AgentRoster, Performance, Tokenomics, + TelegramModes, Roadmap, FAQ, CTA. +- **`/terminal` — живой терминал** (`src/pages/Terminal.tsx`): опрашивает Hyperliquid раз в + ~12 c (`useScreen`), показывает: цену/изменение/фандинг/OI/объём/ATR, сетку индикаторов, + карточку вердикта, голоса агентов с барами уверенности, граф «оркестра» агентов. + +**Seed-фолбэк** (`src/data/seedMarket.ts`): пока грузятся живые данные, мгновенно +показываются детерминированные значения (помечены как не-live), чтобы UI не «висел»; затем +заменяются реальными. + +**Ресёрч-агент (FactsAI)** (`src/lib/factsai.ts`, `AIBrief.tsx`): интеграция с deep-research +API (AI-бриф рынка с цитатами источников). Требует прокси (CORS + ключ на сервере); +секция сама скрывается, если фид недоступен. + +--- + +## 11. Статус реализации (по слоям) + +Технический срез: что считается на живых данных, что симулируется, чего ещё нет. + +| Компонент | Статус | +|---|---| +| L1: свечи, mark/mid, фандинг, OI, объём | ✅ Реально (Hyperliquid live) | +| L1: стакан, лента ликвидаций | ⚪ Не реализовано (только в описании ростера) | +| L2: RSI / MACD / EMA / ATR / Bollinger / funding-Z | ✅ Реально | +| L3: 4 агента (Trend / Breakout / MeanReversion / Funding) | ✅ Реально | +| L4: агрегация → вердикт + риск-параметры | ✅ Реально (формулы, **не LLM**) | +| L5: исполнение | 🟡 Paper / dry-run (реальный PnL, **без ордеров на бирже**) | +| L6: Telegram (уведомления + команды) | ✅ Реально | +| Персистентность | ✅ Реально (JSON; в копирайте сайта упомянут SQLite) | +| Ростер «18 агентов» | ⚪ Метаданные UI; реально голосуют **4** стратегии | +| Кривая капитала / «последние сделки» на сайте | 🟡 Seed-заглушка (live-данные даёт воркер, на сайт ещё не выведены) | +| FactsAI ресёрч-бриф | 🟡 Код есть, **выключен** (нужен прокси; upstream отдаёт ошибку) | +| Коннект кошелька / hold-to-access | ⚪ Косметика (после TGE) | +| Токен $QUANT (цена/контракт/холдеры) | ⚪ pre-TGE (ещё не выпущен) | + +**В разработке (roadmap):** реальное исполнение ордеров на Hyperliquid (live-режим за +флагом); LLM-слой (обоснование вердиктов и осмысленный `/learn` через Claude); вывод +реального трек-рекорда воркера на сайт через `/api/*`. + +--- + +## 12. Конфигурация (env, всё опционально — есть дефолты) + +Читается воркером из `.env.local` (`worker/config.ts`): + +| Переменная | Дефолт | Назначение | +|---|---|---| +| `QUANT_COINS` | `BTC,ETH,SOL` | торгуемая вселенная | +| `QUANT_INTERVAL` | `1h` | таймфрейм свечей | +| `QUANT_POLL_MS` | `60000` | период тика, мс | +| `QUANT_EQUITY_START` | `10000` | стартовый бумажный капитал, $ | +| `QUANT_LEVERAGE` | `5` | плечо для расчёта notional | +| `QUANT_CONF_FLOOR` | `0.55` | минимальная уверенность для входа | +| `QUANT_MAX_OPEN_PER_COIN` | `1` | одновременных позиций на монету | +| `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID` | — | Telegram (без них — вывод в stdout) | + +--- + +## 13. Глоссарий + +- **Perp (перп)** — бессрочный фьючерс, контракт без даты экспирации. +- **Hyperliquid** — децентрализованная биржа перпов с открытым публичным API данных. +- **LONG / SHORT / SKIP** — ставка на рост / на падение / нет сигнала. +- **Funding (фандинг)** — периодический платёж между лонгами и шортами; перекос показывает + «перегретость» толпы. Положительная ставка → лонги платят шортам. +- **OI (Open Interest)** — открытый интерес, сумма открытых контрактов. +- **Mark price** — справедливая цена для расчётов (а не цена последней сделки). +- **RSI** — осциллятор 0–100: >70 перекуплен, <30 перепродан. +- **MACD** — индикатор моментума по разнице скользящих средних. +- **EMA** — экспоненциальная скользящая средняя (тренд). +- **ATR** — мера волатильности (средний диапазон свечи). +- **Bollinger %B** — положение цены внутри полос Боллинджера (0 = низ, 1 = верх). +- **TP / SL** — take-profit / stop-loss. +- **RR (risk/reward)** — отношение потенциальной прибыли к риску (TP/SL). +- **Notional** — номинальный размер позиции в $ (капитал × размер% × плечо). +- **PnL** — прибыль/убыток. **Equity** — капитал. **Profit factor** — сумма прибылей / сумма убытков. +- **Paper-trading / dry-run** — «бумажная» торговля: расчёт как в реале, но без ордеров и денег. +- **Детерминированный** — при одинаковом входе всегда одинаковый результат (нет случайности). +- **pre-TGE** — до выпуска токена (Token Generation Event). +``` diff --git a/docs/token-strategy.md b/docs/token-strategy.md new file mode 100644 index 0000000..4defa1c --- /dev/null +++ b/docs/token-strategy.md @@ -0,0 +1,95 @@ +# Quant Agent ($QUANT) — token utility & engagement strategy + +The honest, shippable answer to: **"what does a user get?"** and **"what keeps them clicking?"** +Built only on what the product really is — a transparent, paper-trading multi-agent terminal on +Hyperliquid (pre-TGE). Vetted by an adversarial honesty/legal pass. + +--- + +## 1. What's the profit for the user? (the one-liner) + +> **Hold $QUANT to see the whole swarm in real time and steer what it watches — access and +> influence, never a promised return.** + +Free users see a **delayed teaser** (one coin, lagged verdict). Holders unlock the **full live +committee** — every coin, every timeframe, real-time verdict flips to their Telegram, and a vote +on what the agents watch next. You're selling **access + influence + speed**, not a payout. + +This is the safe, defensible thesis. The moment the pitch becomes "hold and earn" or "hold for +profitable signals", it becomes a securities + advice problem (see §4). + +--- + +## 2. Token utility — the 5 mechanics to build (in order) + +1. **Hold-to-access tiers (Free vs Holder).** Connect a Solana wallet, read $QUANT balance + client-side, gate UI depth. Free = 1 coin + delayed verdict; Holder = all coins, live verdict, + full agent vote-matrix + reasons. *Two tiers only.* This is the spine — everything hangs off it. +2. **Holder alerts (real-time vs delayed).** The worker already detects verdict flips and Telegram + is already wired. Map wallet→chat_id (one-time sign), DM holders instantly on "committee flipped" + / "high-conviction (>0.9)". Free = delayed digest. *Lowest effort, highest perceived value.* +3. **Premium signal surface.** The brain is parameterized (`CONFIG.coins`/`interval`). Run more + coins + timeframes (15m/4h, top-20 perps); expose via the worker's `/api/state.json`. Breadth is + genuinely compute-scarce, so gating it is honest. +4. **Free prediction game (no staking).** Call LONG/SHORT/SKIP before the next candle, scored vs the + real price move (we already track prices + equity). Cosmetic ranks only. *Retention engine for + non-holders → seeds future holders.* +5. **Governance over the agents/markets.** Balance-weighted snapshot votes on real knobs the config + exposes — which coin/timeframe to add next, agent weights, the confidence floor. Real because the + worker loads the result. *Strongest non-financial reason to hold: influence over a live product.* + +(#6 once liability copy is locked: a **"Open this setup on Hyperliquid"** deep-link that prefills +coin/side/size/TP/SL from the verdict — convenience, the user signs their own trade. Never +auto-execute.) + +--- + +## 3. On-site engagement — features that make people click & return + +Ranked by leverage. Most reuse data we already compute. + +| # | Feature | Why it drives clicks | Effort | +|---|---|---|---| +| 1 | **Public agent track record** (honest paper PnL: equity curve, win rate, last fills) | The #1 "come back" hook — a live scoreboard of how the committee is actually doing. We already persist `equity/closed/equityCurve` in the worker. | M | +| 2 | **Predict-the-verdict game** + seasonal leaderboard | Daily reason to return: "can you read the agents better than the swarm?" | M–L | +| 3 | **Shareable verdict cards** (X/Twitter image) | Free top-of-funnel — every share is an ad with the real signal. | M | +| 4 | **Tune the committee** (weight the 4 agents, re-run live) | Hands-on play → ownership; explore every coin/timeframe. | M | +| 5 | **Multi-coin signal board** (whole universe at a glance) | A reason to scan & click instead of staring at one coin. | M | +| 6 | **Watchlist + price/verdict alerts** | Classic retention — pulls the tab back. | M | +| 7 | **Backtest / replay slider** on the chart | Explorable toy — drag across coins/timeframes. | L | +| 8 | **Live committee ticker / "debate feed"** | Makes the desk feel alive, never static. | S | +| 9 | **Streaks & daily check-in** (cosmetic points) | Connective tissue that turns the above into a daily habit. | M | + +--- + +## 4. Red lines — what NOT to do (honesty + legal) + +The trust of an honest, paper-trading, pre-TGE product is the moat. Avoid: + +- ❌ **Fee-split / revenue-share to holders** (the $AUTR "50% to holders" model) — dividend-like, + strongly implies a security. Differentiate with **access/utility/status, no holder cash flows**. +- ❌ **Points → guaranteed airdrop** framing — "earn now, tokens later" = expectation of profit. + Keep points as **pure status** (rank/badges/cosmetics); never pre-promise token value. +- ❌ **Stake-to-earn / stake-for-rewards** — yield/securities risk. Staking may unlock only + cosmetics/role, never payouts. +- ❌ **"Hold for profitable signals" / any return promise.** Sell access to *data/views*, not outcomes. +- ❌ **Auto-executing trades** on the user's behalf — custody + advice exposure. Deep-link only; the + user signs. +- ❌ **Fake track record / paid-pump signals.** Always label paper/dry-run. + +Everything in §2–§3 is designed to stay on the right side of these lines. + +--- + +## 5. Pre-TGE acquisition (you have no token yet — use it) + +- **Points/season program as pure status now** (no airdrop promise) → daily habit before the token. +- **Public track record + shareable cards** → organic reach proving the agents are real. +- **Telegram alpha bot** (already built) with a waitlist → capture users where crypto lives. +- **Named agent personalities** (Trend / Breakout / Mean-Rev / Funding as a "cast") → memorable, + screenshot-able, turns dry TA into a story. + +--- + +*Strategy synthesised from a multi-agent pass (utility models · engagement · reference meta · +adversarial honesty critic). Not legal advice — have token mechanics + copy reviewed before TGE.* diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ef614d2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..16b6cde --- /dev/null +++ b/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + Quant Agent — a multi-agent quant terminal on Hyperliquid + + + + + + + + + + + + + + + + +

+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..a114ee6 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "larp-quant-agent", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "gen:icons": "node scripts/gen-icons.mjs", + "worker": "tsx worker/loop.ts", + "worker:once": "tsx worker/loop.ts --once" + }, + "dependencies": { + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@tailwindcss/vite": "^4.3.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lightweight-charts": "^5.2.0", + "lucide-react": "^1.17.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-router-dom": "^7.17.0", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "sharp": "^0.34.5", + "svgo": "^4.0.1", + "to-ico": "^1.1.5", + "tsx": "^4.22.4", + "tw-animate-css": "^1.4.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "unplugin-fonts": "^2.0.0", + "vite": "^8.0.12", + "vite-plugin-image-optimizer": "^2.0.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..708f43e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3827 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@radix-ui/react-accordion': + specifier: ^1.2.12 + version: 1.2.12(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@8.0.16) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lightweight-charts: + specifier: ^5.2.0 + version: 5.2.0 + lucide-react: + specifier: ^1.17.0 + version: 1.17.0(react@19.2.7) + react: + specifier: ^19.2.6 + version: 19.2.7 + react-dom: + specifier: ^19.2.6 + version: 19.2.7(react@19.2.7) + react-router-dom: + specifier: ^7.17.0 + version: 7.17.0(react-dom@19.2.7)(react@19.2.7) + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 + +devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.4.1) + '@types/node': + specifier: ^24.12.3 + version: 24.13.0 + '@types/react': + specifier: ^19.2.14 + version: 19.2.16 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.16) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.2(vite@8.0.16) + eslint: + specifier: ^10.3.0 + version: 10.4.1 + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.4.1) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@10.4.1) + globals: + specifier: ^17.6.0 + version: 17.6.0 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + svgo: + specifier: ^4.0.1 + version: 4.0.1 + to-ico: + specifier: ^1.1.5 + version: 1.1.5 + tsx: + specifier: ^4.22.4 + version: 4.22.4 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ~6.0.2 + version: 6.0.3 + typescript-eslint: + specifier: ^8.59.2 + version: 8.60.1(eslint@10.4.1)(typescript@6.0.3) + unplugin-fonts: + specifier: ^2.0.0 + version: 2.0.0(vite@8.0.16) + vite: + specifier: ^8.0.12 + version: 8.0.16(@types/node@24.13.0)(tsx@4.22.4) + vite-plugin-image-optimizer: + specifier: ^2.0.3 + version: 2.0.3(sharp@0.34.5)(svgo@4.0.1)(vite@8.0.16) + +packages: + + /@babel/code-frame@7.29.7: + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + dev: true + + /@babel/compat-data@7.29.7: + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.29.7: + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + 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/core@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.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.29.7: + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + dev: true + + /@babel/helper-compilation-targets@7.29.7: + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-globals@7.29.7: + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-module-imports@7.29.7: + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7): + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-string-parser@7.29.7: + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.29.7: + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.29.7: + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.29.7: + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + dev: true + + /@babel/parser@7.29.7: + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.29.7 + dev: true + + /@babel/template@7.29.7: + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + dev: true + + /@babel/traverse@7.29.7: + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + 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.4.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.29.7: + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + dev: true + + /@capsizecss/unpack@4.0.0: + resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} + engines: {node: '>=18'} + dependencies: + fontkitten: 1.0.3 + dev: true + + /@emnapi/core@1.10.0: + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + requiresBuild: true + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + /@emnapi/runtime@1.10.0: + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + + /@emnapi/wasi-threads@1.2.1: + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + + /@esbuild/aix-ppc64@0.28.0: + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + optional: true + + /@esbuild/android-arm64@0.28.0: + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-arm@0.28.0: + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-x64@0.28.0: + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/darwin-arm64@0.28.0: + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/darwin-x64@0.28.0: + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/freebsd-arm64@0.28.0: + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/freebsd-x64@0.28.0: + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/linux-arm64@0.28.0: + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-arm@0.28.0: + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ia32@0.28.0: + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-loong64@0.28.0: + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-mips64el@0.28.0: + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ppc64@0.28.0: + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-riscv64@0.28.0: + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-s390x@0.28.0: + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-x64@0.28.0: + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/netbsd-arm64@0.28.0: + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + requiresBuild: true + optional: true + + /@esbuild/netbsd-x64@0.28.0: + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + optional: true + + /@esbuild/openbsd-arm64@0.28.0: + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + optional: true + + /@esbuild/openbsd-x64@0.28.0: + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + optional: true + + /@esbuild/openharmony-arm64@0.28.0: + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + requiresBuild: true + optional: true + + /@esbuild/sunos-x64@0.28.0: + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + requiresBuild: true + optional: true + + /@esbuild/win32-arm64@0.28.0: + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-ia32@0.28.0: + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-x64@0.28.0: + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@eslint-community/eslint-utils@4.9.1(eslint@10.4.1): + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 10.4.1 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.12.2: + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/config-array@0.23.5: + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/config-helpers@0.6.0: + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + dependencies: + '@eslint/core': 1.2.1 + dev: true + + /@eslint/core@1.2.1: + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + dependencies: + '@types/json-schema': 7.0.15 + dev: true + + /@eslint/js@10.0.1(eslint@10.4.1): + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + dependencies: + eslint: 10.4.1 + dev: true + + /@eslint/object-schema@3.0.5: + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + dev: true + + /@eslint/plugin-kit@0.7.2: + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + dev: true + + /@humanfs/core@0.19.2: + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + dependencies: + '@humanfs/types': 0.15.0 + dev: true + + /@humanfs/node@0.16.8: + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + dev: true + + /@humanfs/types@0.15.0: + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/retry@0.4.3: + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + dev: true + + /@img/colour@1.1.0: + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + dev: true + + /@img/sharp-darwin-arm64@0.34.5: + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + dev: true + optional: true + + /@img/sharp-darwin-x64@0.34.5: + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + dev: true + optional: true + + /@img/sharp-libvips-darwin-arm64@1.2.4: + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-darwin-x64@1.2.4: + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-linux-arm64@1.2.4: + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-linux-arm@1.2.4: + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-linux-ppc64@1.2.4: + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-linux-riscv64@1.2.4: + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-linux-s390x@1.2.4: + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-linux-x64@1.2.4: + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-linuxmusl-arm64@1.2.4: + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-libvips-linuxmusl-x64@1.2.4: + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-linux-arm64@0.34.5: + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + dev: true + optional: true + + /@img/sharp-linux-arm@0.34.5: + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + dev: true + optional: true + + /@img/sharp-linux-ppc64@0.34.5: + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + dev: true + optional: true + + /@img/sharp-linux-riscv64@0.34.5: + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + dev: true + optional: true + + /@img/sharp-linux-s390x@0.34.5: + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + dev: true + optional: true + + /@img/sharp-linux-x64@0.34.5: + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + dev: true + optional: true + + /@img/sharp-linuxmusl-arm64@0.34.5: + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + dev: true + optional: true + + /@img/sharp-linuxmusl-x64@0.34.5: + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + dev: true + optional: true + + /@img/sharp-wasm32@0.34.5: + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@emnapi/runtime': 1.10.0 + dev: true + optional: true + + /@img/sharp-win32-arm64@0.34.5: + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-win32-ia32@0.34.5: + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@img/sharp-win32-x64@0.34.5: + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@jridgewell/gen-mapping@0.3.13: + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + /@jridgewell/remapping@2.3.5: + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/sourcemap-codec@1.5.5: + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + /@jridgewell/trace-mapping@0.3.31: + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + /@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + requiresBuild: true + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + dev: true + + /@oxc-project/types@0.133.0: + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + /@radix-ui/primitive@1.1.3: + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + dev: false + + /@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-context@1.1.2(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.16)(react@19.2.7) + dev: false + + /@radix-ui/react-direction@1.1.1(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-id@1.1.1(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-slot@1.2.3(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-slot@1.2.4(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.16)(react-dom@19.2.7)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + dev: false + + /@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + dev: false + + /@rolldown/binding-android-arm64@1.0.3: + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@rolldown/binding-darwin-arm64@1.0.3: + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@rolldown/binding-darwin-x64@1.0.3: + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@rolldown/binding-freebsd-x64@1.0.3: + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + + /@rolldown/binding-linux-arm-gnueabihf@1.0.3: + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@rolldown/binding-linux-arm64-gnu@1.0.3: + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rolldown/binding-linux-arm64-musl@1.0.3: + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rolldown/binding-linux-ppc64-gnu@1.0.3: + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true + + /@rolldown/binding-linux-s390x-gnu@1.0.3: + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@rolldown/binding-linux-x64-gnu@1.0.3: + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rolldown/binding-linux-x64-musl@1.0.3: + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rolldown/binding-openharmony-arm64@1.0.3: + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + requiresBuild: true + optional: true + + /@rolldown/binding-wasm32-wasi@1.0.3: + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + /@rolldown/binding-win32-arm64-msvc@1.0.3: + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@rolldown/binding-win32-x64-msvc@1.0.3: + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@rolldown/pluginutils@1.0.1: + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + /@tailwindcss/node@4.3.0: + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.2 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + dev: false + + /@tailwindcss/oxide-android-arm64@4.3.0: + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-darwin-arm64@4.3.0: + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-darwin-x64@4.3.0: + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-freebsd-x64@4.3.0: + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0: + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-arm64-gnu@4.3.0: + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-arm64-musl@4.3.0: + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-x64-gnu@4.3.0: + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-linux-x64-musl@4.3.0: + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-wasm32-wasi@4.3.0: + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + requiresBuild: true + dev: false + optional: true + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + /@tailwindcss/oxide-win32-arm64-msvc@4.3.0: + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide-win32-x64-msvc@4.3.0: + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@tailwindcss/oxide@4.3.0: + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + dev: false + + /@tailwindcss/vite@4.3.0(vite@8.0.16): + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 8.0.16(@types/node@24.13.0)(tsx@4.22.4) + dev: false + + /@tybys/wasm-util@0.10.2: + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + + /@types/esrecurse@4.3.1: + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + dev: true + + /@types/estree@1.0.9: + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/node@24.13.0: + resolution: {integrity: sha512-5vtOqGQr4NJKeEzV441FcOi2MeG9UTWq9LqVLGneDdu4vlX17H8kQ2PA2UmNwCUGPVDj4oBjNhS7ReVEIWJJrg==} + dependencies: + undici-types: 7.18.2 + + /@types/react-dom@19.2.3(@types/react@19.2.16): + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + dependencies: + '@types/react': 19.2.16 + + /@types/react@19.2.16: + resolution: {integrity: sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==} + dependencies: + csstype: 3.2.3 + + /@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1)(eslint@10.4.1)(typescript@6.0.3): + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@8.60.1(eslint@10.4.1)(typescript@6.0.3): + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + dependencies: + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/project-service@8.60.1(typescript@6.0.3): + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@8.60.1: + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + dev: true + + /@typescript-eslint/tsconfig-utils@8.60.1(typescript@6.0.3): + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + dependencies: + typescript: 6.0.3 + dev: true + + /@typescript-eslint/type-utils@8.60.1(eslint@10.4.1)(typescript@6.0.3): + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.4.1 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@8.60.1: + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /@typescript-eslint/typescript-estree@8.60.1(typescript@6.0.3): + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + dependencies: + '@typescript-eslint/project-service': 8.60.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.2 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@8.60.1(eslint@10.4.1)(typescript@6.0.3): + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/visitor-keys@8.60.1: + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.60.1 + eslint-visitor-keys: 5.0.1 + dev: true + + /@vitejs/plugin-react@6.0.2(vite@8.0.16): + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.16(@types/node@24.13.0)(tsx@4.22.4) + dev: true + + /acorn-jsx@5.3.2(acorn@8.16.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.16.0 + dev: true + + /acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + + /aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + dependencies: + tslib: 2.8.1 + dev: false + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + + /asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + + /aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + dev: true + + /aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + dev: true + + /balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + dev: true + + /baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + dev: true + + /bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + dev: true + + /bignumber.js@2.4.0: + resolution: {integrity: sha512-uw4ra6Cv483Op/ebM0GBKKfxZlSmn6NgFRby5L3yGTlunLj53KQgndDlqy2WVFOwgvurocApYkSud0aO+mvrpQ==} + dev: true + + /bmp-js@0.0.1: + resolution: {integrity: sha512-OS74Rlt0Aynu2mTPmY9RZOUOXlqWecFIILFXr70vv16/xCZnFxvri9IKkF1IGxQ8r9dOE62qGNpKxXx8Lko8bg==} + dev: true + + /bmp-js@0.0.3: + resolution: {integrity: sha512-epsm3Z92j5xwek9p97pVw3KbsNc0F4QnbYh+N93SpbJYuHFQQ/UAh6K+bKFGyLePH3Hudtl/Sa95Quqp0gX8IQ==} + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + dependencies: + balanced-match: 4.0.4 + dev: true + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: true + + /browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.367 + node-releases: 2.0.47 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + dev: true + + /buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + dev: true + + /buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + dependencies: + buffer-alloc-unsafe: 1.1.0 + buffer-fill: 1.0.0 + dev: true + + /buffer-equal@0.0.1: + resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} + engines: {node: '>=0.4.0'} + dev: true + + /buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + dev: true + + /caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + dev: true + + /caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: true + + /centra@2.7.0: + resolution: {integrity: sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==} + dependencies: + follow-redirects: 1.16.0 + transitivePeerDependencies: + - debug + dev: true + + /class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + dependencies: + clsx: 2.1.1 + dev: false + + /clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: true + + /confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + dev: false + + /core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + dev: true + + /cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + dev: true + + /css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + dev: true + + /css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + dev: true + + /css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + dev: true + + /csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + css-tree: 2.2.1 + dev: true + + /csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + /dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + dev: true + + /debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + + /detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + /detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dev: false + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + dev: true + + /electron-to-chromium@1.5.367: + resolution: {integrity: sha512-4Mk/mrynCNQ+atY40D3UpmhLWB6AHMbYMlIrPhHcMF6x0L7O0b052FCAsxw1LlaR++UFuNg3D/A6XCuGDa0guQ==} + dev: true + + /enhanced-resolve@5.22.2: + resolution: {integrity: sha512-0rxICaFZ7NQho/sHely2bvOPRP0Eu2B0NZ9zM54YvRvWMn7jfz3DmnOZDR9LlXDdDcqntAVc6Hfy4gr/tdH/Ag==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + dev: false + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /es6-promise@3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + dev: true + + /esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-plugin-react-hooks@7.1.1(eslint@10.4.1): + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 10.4.1 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-react-refresh@0.5.2(eslint@10.4.1): + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + dependencies: + eslint: 10.4.1 + dev: true + + /eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + dev: true + + /eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + dev: true + + /esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.9 + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + dev: true + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: true + + /extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + dev: true + + /fancy-canvas@2.1.0: + resolution: {integrity: sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + dependencies: + reusify: 1.1.0 + dev: true + + /fdir@6.5.0(picomatch@4.0.4): + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + dependencies: + picomatch: 4.0.4 + + /file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + dependencies: + flat-cache: 4.0.1 + dev: true + + /file-type@3.9.0: + resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} + engines: {node: '>=0.10.0'} + dev: true + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + dev: true + + /flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + dev: true + + /follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: true + + /fontaine@0.8.0: + resolution: {integrity: sha512-eek1GbzOdWIj9FyQH/emqW1aEdfC3lYRCHepzwlFCm5T77fBSRSyNRKE6/antF1/B1M+SfJXVRQTY9GAr7lnDg==} + engines: {node: '>=18.12.0'} + dependencies: + '@capsizecss/unpack': 4.0.0 + css-tree: 3.2.1 + magic-regexp: 0.10.0 + magic-string: 0.30.21 + pathe: 2.0.3 + ufo: 1.6.4 + unplugin: 2.3.11 + dev: true + + /fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + dependencies: + tiny-inflate: 1.0.3 + dev: true + + /forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + dev: true + + /form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + dev: false + + /get-stream@2.3.1: + resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==} + engines: {node: '>=0.10.0'} + dependencies: + object-assign: 4.1.1 + pinkie-promise: 2.0.1 + dev: true + + /getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + dependencies: + assert-plus: 1.0.0 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + dependencies: + min-document: 2.19.2 + process: 0.11.10 + dev: true + + /globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + + /har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + dev: true + + /har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + dependencies: + ajv: 6.15.0 + har-schema: 2.0.0 + dev: true + + /hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + dev: true + + /hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + dependencies: + hermes-estree: 0.25.1 + dev: true + + /http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + dev: true + + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + dev: true + + /ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + dev: true + + /image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /ip-regex@1.0.3: + resolution: {integrity: sha512-HjpCHTuxbR/6jWJroc/VN+npo5j0T4Vv2TAI5qdEHQx7hsL767MeccGFSsLtF694EiZKTSEqgoeU6DtGFCcuqQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-function@1.0.2: + resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + dev: true + + /jimp@0.2.28: + resolution: {integrity: sha512-9HT7DA279xkTlry2oG30s6AtOUglNiY2UdyYpj0yNI4/NBv8PmdNC0gcldgMU4HqvbUlrM3+v+6GaHnTkH23JQ==} + dependencies: + bignumber.js: 2.4.0 + bmp-js: 0.0.3 + es6-promise: 3.3.1 + exif-parser: 0.1.12 + file-type: 3.9.0 + jpeg-js: 0.2.0 + load-bmfont: 1.4.2 + mime: 1.6.0 + mkdirp: 0.5.1 + pixelmatch: 4.0.2 + pngjs: 3.4.0 + read-chunk: 1.0.1 + request: 2.88.2 + stream-to-buffer: 0.1.0 + tinycolor2: 1.6.0 + url-regex: 3.2.0 + transitivePeerDependencies: + - debug + dev: true + + /jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + dev: false + + /jpeg-js@0.1.2: + resolution: {integrity: sha512-CiRVjMKBUp6VYtGwzRjrdnro41yMwKGOWdP9ATXqmixdz2n7KHNwdqthTYSSbOKj/Ha79Gct1sA8ZQpse55TYA==} + dev: true + + /jpeg-js@0.2.0: + resolution: {integrity: sha512-Ni9PffhJtYtdD7VwxH6V2MnievekGfUefosGCHadog0/jAevRu6HPjYeMHbUemn0IPE8d4wGa8UsOGsX+iKy2g==} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + dev: true + + /jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: true + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + + /lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + /lightweight-charts@5.2.0: + resolution: {integrity: sha512-ey3Vas8UhV06ni+LT9TA1nEe4y8So4Mi6CL/oarNHFMyTktz/xy8e8+oh04Q//eO3t6etvFXgayz2fClyFQb5w==} + dependencies: + fancy-canvas: 2.1.0 + dev: false + + /load-bmfont@1.4.2: + resolution: {integrity: sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==} + dependencies: + buffer-equal: 0.0.1 + mime: 1.6.0 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.6 + phin: 3.7.1 + xhr: 2.6.0 + xtend: 4.0.2 + transitivePeerDependencies: + - debug + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lucide-react@1.17.0(react@19.2.7): + resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + dependencies: + react: 19.2.7 + dev: false + + /magic-regexp@0.10.0: + resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.21 + mlly: 1.8.2 + regexp-tree: 0.1.27 + type-level-regexp: 0.1.17 + ufo: 1.6.4 + unplugin: 2.3.11 + dev: true + + /magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + /mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + dev: true + + /mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} + dependencies: + dom-walk: 0.1.2 + dev: true + + /minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + dependencies: + brace-expansion: 5.0.6 + dev: true + + /minimist@0.0.8: + resolution: {integrity: sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==} + dev: true + + /mkdirp@0.5.1: + resolution: {integrity: sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + hasBin: true + dependencies: + minimist: 0.0.8 + dev: true + + /mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /node-releases@2.0.47: + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + engines: {node: '>=18'} + dev: true + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + dev: true + + /parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + dev: true + + /parse-bmfont-xml@1.1.6: + resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.5.0 + dev: true + + /parse-headers@2.0.6: + resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==} + dev: true + + /parse-png@1.1.2: + resolution: {integrity: sha512-Ge6gDV9T5zhkWHmjvnNiyhPTCIoY7W+FC7qWPtuL2lIGZAFxxqTRG/ouEXsH9qkw+HzYiPEU/tFcxOCEDTP1Yw==} + engines: {node: '>=4'} + dependencies: + pngjs: 3.4.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + dev: true + + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: true + + /phin@3.7.1: + resolution: {integrity: sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==} + engines: {node: '>= 8'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + dependencies: + centra: 2.7.0 + transitivePeerDependencies: + - debug + dev: true + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + /picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + dev: true + + /picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + /pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + dependencies: + pinkie: 2.0.4 + dev: true + + /pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + dev: true + + /pixelmatch@4.0.2: + resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} + hasBin: true + dependencies: + pngjs: 3.4.0 + dev: true + + /pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + dev: true + + /pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + dev: true + + /postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: true + + /psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + dependencies: + punycode: 2.3.1 + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /qs@6.5.5: + resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} + engines: {node: '>=0.6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /react-dom@19.2.7(react@19.2.7): + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + dev: false + + /react-remove-scroll-bar@2.3.8(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + react-style-singleton: 2.2.3(@types/react@19.2.16)(react@19.2.7) + tslib: 2.8.1 + dev: false + + /react-remove-scroll@2.7.2(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.16)(react@19.2.7) + react-style-singleton: 2.2.3(@types/react@19.2.16)(react@19.2.7) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.16)(react@19.2.7) + use-sidecar: 1.1.3(@types/react@19.2.16)(react@19.2.7) + dev: false + + /react-router-dom@7.17.0(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-router: 7.17.0(react-dom@19.2.7)(react@19.2.7) + dev: false + + /react-router@7.17.0(react-dom@19.2.7)(react@19.2.7): + resolution: {integrity: sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + dependencies: + cookie: 1.1.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + set-cookie-parser: 2.7.2 + dev: false + + /react-style-singleton@2.2.3(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + get-nonce: 1.0.1 + react: 19.2.7 + tslib: 2.8.1 + dev: false + + /react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + dev: false + + /read-chunk@1.0.1: + resolution: {integrity: sha512-5NLTTdX45dKFtG8CX5pKmvS9V5u9wBE+gkklN7xhDuhq3pA2I4O7ALfKxosCMcLHOhkxj6GNacZhfXtp5nlCdg==} + engines: {node: '>=0.10.0'} + dev: true + + /regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + dev: true + + /request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.5 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + dev: true + + /resize-img@1.1.2: + resolution: {integrity: sha512-/4nKUmuNPuM6gYTWad136ica81baOVjpesgv8FGaIvP0KWcbCWahOWBKaM4tFoM+aVcSA+qQDg28pcnIzFRpJw==} + engines: {node: '>=4'} + dependencies: + bmp-js: 0.0.1 + file-type: 3.9.0 + get-stream: 2.3.1 + jimp: 0.2.28 + jpeg-js: 0.1.2 + parse-png: 1.1.2 + transitivePeerDependencies: + - debug + dev: true + + /reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + dev: true + + /scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.8.2: + resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + dev: false + + /sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + requiresBuild: true + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.2 + 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 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + /sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + dev: true + + /stream-to-buffer@0.1.0: + resolution: {integrity: sha512-Da4WoKaZyu3nf+bIdIifh7IPkFjARBnBK+pYqn0EUJqksjV9afojjaCCHUemH30Jmu7T2qcKvlZm2ykN38uzaw==} + engines: {node: '>= 0.8'} + dependencies: + stream-to: 0.2.2 + dev: true + + /stream-to@0.2.2: + resolution: {integrity: sha512-Kg1BSDTwgGiVMtTCJNlo7kk/xzL33ZuZveEBRt6rXw+f1WLK/8kmz2NVCT/Qnv0JkV85JOHcLhD82mnXsR3kPw==} + engines: {node: '>= 0.10.0'} + dev: true + + /svgo@4.0.1: + resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + engines: {node: '>=16'} + hasBin: true + dependencies: + commander: 11.1.0 + css-select: 5.2.2 + css-tree: 3.2.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + dev: true + + /tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + dev: false + + /tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + dev: false + + /tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + dev: false + + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: true + + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + dev: true + + /tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + /to-ico@1.1.5: + resolution: {integrity: sha512-5kIh7m7bkIlqIESEZkL8gAMMzucXKfPe3hX2FoDY5HEAfD9OJU+Qh9b6Enp74w0qRcxVT5ejss66PHKqc3AVkg==} + engines: {node: '>=4'} + dependencies: + arrify: 1.0.1 + buffer-alloc: 1.2.0 + image-size: 0.5.5 + parse-png: 1.1.2 + resize-img: 1.1.2 + transitivePeerDependencies: + - debug + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + dev: true + + /ts-api-utils@2.5.0(typescript@6.0.3): + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + dependencies: + typescript: 6.0.3 + dev: true + + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + requiresBuild: true + + /tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.28.0 + optionalDependencies: + fsevents: 2.3.3 + + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + dev: true + + /tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-level-regexp@0.1.17: + resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} + dev: true + + /typescript-eslint@8.60.1(eslint@10.4.1)(typescript@6.0.3): + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1)(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1)(typescript@6.0.3) + eslint: 10.4.1 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + dev: true + + /undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + /unplugin-fonts@2.0.0(vite@8.0.16): + resolution: {integrity: sha512-MjyPgq9UVXYSQlcqG5Y/tYFCJvK0unURtT8+PdhrYV7u/8uwycqqLj5ouwpZ+oNQXRvemYqgnSLxGUv00r984g==} + peerDependencies: + '@nuxt/kit': ^3.0.0 || ^4.0.0 + vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + dependencies: + css-tree: 3.2.1 + fast-glob: 3.3.3 + fontaine: 0.8.0 + magic-string: 0.30.21 + pathe: 2.0.3 + unplugin: 3.0.0 + vite: 8.0.16(@types/node@24.13.0)(tsx@4.22.4) + dev: true + + /unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + dev: true + + /unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + dev: true + + /update-browserslist-db@1.2.3(browserslist@4.28.2): + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /url-regex@3.2.0: + resolution: {integrity: sha512-dQ9cJzMou5OKr6ZzfvwJkCq3rC72PNXhqz0v3EIhF4a3Np+ujr100AhUx2cKx5ei3iymoJpJrPB3sVSEMdqAeg==} + engines: {node: '>=0.10.0'} + dependencies: + ip-regex: 1.0.3 + dev: true + + /use-callback-ref@1.3.3(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + react: 19.2.7 + tslib: 2.8.1 + dev: false + + /use-sidecar@1.1.3(@types/react@19.2.16)(react@19.2.7): + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 19.2.16 + detect-node-es: 1.1.0 + react: 19.2.7 + tslib: 2.8.1 + dev: false + + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + dev: true + + /verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + dev: true + + /vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(svgo@4.0.1)(vite@8.0.16): + resolution: {integrity: sha512-1vrFOTcpSvv6DCY7h8UXab4wqMAjTJB/ndOzG/Kmj1oDOuPF6mbjkNQoGzzCEYeWGe7qU93jc8oQqvoJ57al3A==} + engines: {node: '>=18.17.0'} + peerDependencies: + sharp: '>=0.34.0' + svgo: '>=4' + vite: '>=5' + peerDependenciesMeta: + sharp: + optional: true + svgo: + optional: true + dependencies: + ansi-colors: 4.1.3 + pathe: 2.0.3 + sharp: 0.34.5 + svgo: 4.0.1 + vite: 8.0.16(@types/node@24.13.0)(tsx@4.22.4) + dev: true + + /vite@8.0.16(@types/node@24.13.0)(tsx@4.22.4): + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + dependencies: + '@types/node': 24.13.0 + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + tsx: 4.22.4 + optionalDependencies: + fsevents: 2.3.3 + + /webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /xhr@2.6.0: + resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} + dependencies: + global: 4.4.0 + is-function: 1.0.2 + parse-headers: 2.0.6 + xtend: 4.0.2 + dev: true + + /xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + dev: true + + /xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + dev: true + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: true + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true + + /zod-validation-error@4.0.2(zod@4.4.3): + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + dependencies: + zod: 4.4.3 + dev: true + + /zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + dev: true diff --git a/proxy/factsai-worker.js b/proxy/factsai-worker.js new file mode 100644 index 0000000..dd33249 --- /dev/null +++ b/proxy/factsai-worker.js @@ -0,0 +1,53 @@ +/** + * Cloudflare Worker proxy for the FactsAI research API. + * + * Why: a static site can't call FactsAI directly — the API sends no + * Access-Control-Allow-Origin (browser blocks it) and a client key would be + * public. This worker holds the key server-side and adds CORS headers. + * + * Deploy: + * 1) npm i -g wrangler && wrangler login + * 2) wrangler secret put FACTSAI_KEY # paste the polyd_... key + * 3) wrangler deploy proxy/factsai-worker.js --name quant-factsai + * 4) set VITE_FACTSAI_PROXY=https://quant-factsai..workers.dev in .env.local, rebuild. + * + * Note: FactsAI must also fix their upstream 500 ("Service configuration error") + * before real answers come back — this proxy only fixes CORS + key exposure. + */ +const ALLOW_ORIGIN = '*' // tighten to your domain in production + +export default { + async fetch(request, env) { + const cors = { + 'Access-Control-Allow-Origin': ALLOW_ORIGIN, + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', + } + if (request.method === 'OPTIONS') return new Response(null, { headers: cors }) + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405, headers: cors }) + } + try { + const body = await request.text() + const upstream = await fetch('https://deep-research-api.degodmode3-33.workers.dev/answer', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${env.FACTSAI_KEY}`, + }, + body, + }) + const text = await upstream.text() + return new Response(text, { + status: upstream.status, + headers: { ...cors, 'Content-Type': 'application/json' }, + }) + } catch (e) { + return new Response(JSON.stringify({ success: false, error: String(e) }), { + status: 502, + headers: { ...cors, 'Content-Type': 'application/json' }, + }) + } + }, +} diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b52426a0e996e0a5121b7b19ad616ca1b2bfcf GIT binary patch literal 8157 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 Da|)+` literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8d154918fe0cf80b6fa491ab7fcbed8de1b6be23 GIT binary patch literal 14510 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 Ar2qf` literal 0 HcmV?d00001 diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..28eb37a --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..76f1ce6cb370efde05ac452ec7bcc2a3b30d5a95 GIT binary patch literal 30308 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 z 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%0A?ERen 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 NQc_N$@R{C+{{zr sharp(svg, { density: 384 }).resize(s, s).png().toBuffer()), +) +writeFileSync(join(pub, 'favicon.ico'), await toIco(pngs)) + +// apple touch + pwa icon +await sharp(svg, { density: 512 }).resize(180, 180).png().toFile(join(pub, 'apple-touch-icon.png')) +await sharp(svg, { density: 512 }).resize(512, 512).png().toFile(join(pub, 'icon-512.png')) + +// social card (og:image) — Quant Agent (wireframe globe) +const globe = (cx, cy, r, sw) => + ` + + + + + + + + + + + ` +const og = ` + + + ${Array.from({ length: 12 }, (_, i) => ``).join('')} + ${Array.from({ length: 7 }, (_, i) => ``).join('')} + + ${globe(955, 315, 210, 5)} + + + + + + ${globe(0, 0, 19, 2.6)} + Quant Agent + Four agents argue. + One verdict. + A multi-agent quant terminal for Hyperliquid perps — live data, TA, LONG / SHORT / SKIP. + + + LIVE · HYPERLIQUID + PAPER · DRY-RUN + +` +await sharp(Buffer.from(og)).png().toFile(join(pub, 'og.png')) + +console.log('✓ icons + og generated') diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..6cc1802 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,65 @@ +{ + "version": 1, + "skills": { + "add-x-tweet": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/add-x-tweet/SKILL.md", + "computedHash": "8c8da25f1134f021daace5c537b933ec47e5e9f7c1e068c043f57666b7f6fe64" + }, + "bklit-playground": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/bklit-playground/SKILL.md", + "computedHash": "0ef6bb543bcb471fb57727216a28020d3da266f5d21be0b27b1163aa0e6f3588" + }, + "bklit-ship": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/bklit-ship/SKILL.md", + "computedHash": "08ebf48f1b79f567a0a93606cfc77ea2a7dc13009f23e9d1bf51b656faeb89b5" + }, + "bklit-studio": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/bklit-studio/SKILL.md", + "computedHash": "caba1f2d3b6e6588095df0f604289d7bb2f8eb84942b8436ffa4cdd97df8cf1a" + }, + "bklit-studio-chart-performance": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/bklit-studio-chart-performance/SKILL.md", + "computedHash": "093d2ee09ed0a6ca307e2091ccc393896c49c8b0d614cc119b1222ba49dce988" + }, + "bklit-ui": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": "skills/bklit-ui/SKILL.md", + "computedHash": "ace89f8b9a6d5d8f0367307f886cb9654065356db537df7b57b3d849aa503534" + }, + "pr-open": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/pr-open/SKILL.md", + "computedHash": "90e2abbb893c87e951d1f3952d87c51e2c23c9d64423129f9b038ffc070cdb55" + }, + "turborepo": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/turborepo/SKILL.md", + "computedHash": "65c73e2cbc0a64c52f8af30421789d59ba06365c27818beaf3e0f75ab72a28e0" + }, + "unit-tests": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/unit-tests/SKILL.md", + "computedHash": "f304a5182f12945cc697ada0e81c0e349c6b1ea53876b094071f180294cbaa41" + }, + "wiki-llms-txt": { + "source": "bklit/bklit-ui", + "sourceType": "github", + "skillPath": ".agents/skills/wiki-llms-text/SKILL.md", + "computedHash": "34cc980ec4dce21191b75d15bfd40818306f878dd37fd558a6e65d7883b7dd9d" + } + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..9878102 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,20 @@ +import { lazy, Suspense } from 'react' +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import { Landing } from '@/pages/Landing' + +const Terminal = lazy(() => import('@/pages/Terminal').then((m) => ({ default: m.Terminal }))) + +function App() { + return ( + + }> + + } /> + } /> + + + + ) +} + +export default App diff --git a/src/components/Onboarding.tsx b/src/components/Onboarding.tsx new file mode 100644 index 0000000..1cf6eb5 --- /dev/null +++ b/src/components/Onboarding.tsx @@ -0,0 +1,124 @@ +import { useEffect, useState } from 'react' +import { LogoMark } from '@/components/brand/Logo' + +/** Fire this anywhere to replay the intro tour: window.dispatchEvent(new Event('quant:tour')) */ +export const TOUR_EVENT = 'quant:tour' + +const STEPS = [ + { + title: 'Quant Agent', + body: 'A transparent quant terminal for Hyperliquid perps. Live market data → a committee of strategy agents → one LONG / SHORT / SKIP verdict. Paper-traded — no real orders.', + }, + { + title: 'The orchestra', + body: 'Six layers run end to end: data → analysis → signal → decision → execution → comms. Hover any agent node to see what it does and whether it’s live, computed or planned.', + }, + { + title: 'Operate it', + body: 'Switch coin & timeframe up top · hover any field to inspect it · click an agent to pin it across the views · ↻ to refresh. Keys: 1 / 2 / 3 coin · r refresh · Esc.', + }, +] + +export function Onboarding() { + const [open, setOpen] = useState(() => { + try { + return localStorage.getItem('quant.onboarded.v1') !== '1' + } catch { + return true + } + }) + const [step, setStep] = useState(0) + + const done = () => { + try { + localStorage.setItem('quant.onboarded.v1', '1') + } catch { + /* ignore */ + } + setOpen(false) + } + + // allow the Help panel (or anything) to replay the tour + useEffect(() => { + const replay = () => { + setStep(0) + setOpen(true) + } + window.addEventListener(TOUR_EVENT, replay) + return () => window.removeEventListener(TOUR_EVENT, replay) + }, []) + + // Esc to dismiss + useEffect(() => { + if (!open) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.stopPropagation() + done() + } + } + window.addEventListener('keydown', onKey, true) + return () => window.removeEventListener('keydown', onKey, true) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]) + + if (!open) return null + + const last = step === STEPS.length - 1 + const s = STEPS[step] + + return ( +

+
+
+
+
+ + + Quant Agent + + + {step + 1} / {STEPS.length} + +
+ +
+

{s.title}

+

+ {s.body} +

+
+ +
+
+ {STEPS.map((_, i) => ( +
+
+ {!last && ( + + )} + +
+
+
+
+ ) +} diff --git a/src/components/brand/Logo.tsx b/src/components/brand/Logo.tsx new file mode 100644 index 0000000..906b885 --- /dev/null +++ b/src/components/brand/Logo.tsx @@ -0,0 +1,32 @@ +import { cn } from '@/lib/utils' + +/** Wireframe-globe mark — the Quant Agent identity (white line-art globe + two cores). */ +export function LogoMark({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + ) +} + +export function Wordmark({ className }: { className?: string }) { + return ( + + + + Quant Agent + + + ) +} diff --git a/src/components/landing/AgentFlow.tsx b/src/components/landing/AgentFlow.tsx new file mode 100644 index 0000000..241625f --- /dev/null +++ b/src/components/landing/AgentFlow.tsx @@ -0,0 +1,181 @@ +import { useEffect, useState } from 'react' +import { ConfidenceRing } from '@/components/viz/ConfidenceRing' + +/** + * Clear, narrated walkthrough of one decision — 5 numbered steps with a plain-language + * caption that updates as it plays. Deterministic canned LONG scenario; mirrors the real + * terminal so the explainer never overstates what the system does. + */ + +const STEPS = [ + { n: 1, name: 'Data', say: 'Pull live candles, funding and open interest from Hyperliquid.' }, + { n: 2, name: 'Analysis', say: 'Recompute the indicators — RSI, MACD, EMA, ATR, Bollinger, funding-Z.' }, + { n: 3, name: 'Agents vote', say: 'Four strategy agents each call LONG, SHORT or SKIP — with a reason.' }, + { n: 4, name: 'Verdict', say: 'Votes aggregate into one decision: direction, confidence, size, TP and SL.' }, + { n: 5, name: 'Telegram', say: 'The call is paper-traded and pushed to Telegram. No real orders.' }, +] + +const AGENTS = [ + { label: 'Trend', reads: 'EMA 9/21 · RSI', dir: 'LONG', score: 0.82, reason: 'EMA9 above EMA21, RSI 58' }, + { label: 'Breakout', reads: 'Bollinger %B', dir: 'SKIP', score: 0.12, reason: 'Price mid-band (%B 0.55)' }, + { label: 'Mean-Rev', reads: 'RSI extremes', dir: 'SKIP', score: 0.05, reason: 'RSI 58 not extreme' }, + { label: 'Funding', reads: 'funding-Z', dir: 'LONG', score: 0.61, reason: 'Funding −Z, shorts crowded' }, +] as const + +const col = (d: string) => + d === 'LONG' ? 'var(--color-long)' : d === 'SHORT' ? 'var(--color-short)' : 'var(--color-muted)' +const bg = (d: string) => + d === 'LONG' ? 'var(--color-long-dim)' : d === 'SHORT' ? 'var(--color-short-dim)' : 'var(--color-track)' + +export function AgentFlow() { + const [step, setStep] = useState(0) + useEffect(() => { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + setStep(4) + return + } + const id = setInterval(() => setStep((s) => (s + 1) % STEPS.length), 2000) + return () => clearInterval(id) + }, []) + + const agentsOn = step >= 2 + const verdictOn = step >= 3 + const tgOn = step >= 4 + + return ( +
+ {/* numbered step rail */} +
+ {STEPS.map((s, i) => { + const on = i === step + const done = i < step + return ( +
+ + {i < STEPS.length - 1 && } +
+ ) + })} +
+ + {/* plain-language narration for the active step */} +
+ Step {STEPS[step].n}/5 +

+ {STEPS[step].say} +

+
+ + {/* visual: committee → verdict (syncs to the steps) */} +
+ {/* committee */} +
+
Agent committee · 4 strategies
+
+ {AGENTS.map((a) => { + const signal = a.dir !== 'SKIP' + return ( +
+
+
{a.label}
+
{a.reads}
+
+
+
+
+
+ {!signal && ( +
+ )} +
+ + {signal ? a.dir : 'skip'} + +
+
+ ) + })} +
+
+ + {/* verdict */} +
+
+
Verdict
+
+
+
+ {verdictOn ? 'LONG' : '—'} +
+
+ 2 of 4 agents align · the rest skip +
+
+ +
+
+ {[ + ['Size', '2.6%', 'var(--color-bone)'], + ['TP', '+1.7%', 'var(--color-long)'], + ['SL', '−0.9%', 'var(--color-short)'], + ].map(([l, v, c]) => ( +
+
{l}
+
{v}
+
+ ))} +
+
+ 🟢 + LONG BTC-PERP @ $63,540 · TP +1.7% · SL −0.9% · 81% + → telegram +
+
+
+
+ ) +} diff --git a/src/components/landing/GlobeCanvas.tsx b/src/components/landing/GlobeCanvas.tsx new file mode 100644 index 0000000..c629476 --- /dev/null +++ b/src/components/landing/GlobeCanvas.tsx @@ -0,0 +1,175 @@ +import { useEffect, useRef } from 'react' + +/** + * Rotating wireframe globe with a live network overlay — the "global AI agent" hero, + * matching the brand banner. Latitude/longitude lines (depth-shaded) + pulsing network + * nodes. Performance-first: thin lines, no shadowBlur, ~30fps, DPR capped, paused offscreen. + */ +export function GlobeCanvas({ className }: { className?: string }) { + const ref = useRef(null) + + useEffect(() => { + const canvas = ref.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches + const dpr = Math.min(window.devicePixelRatio || 1, 1.5) + const tilt = -0.42 + let w = 0 + let h = 0 + let raf = 0 + let angle = 0 + let visible = true + + const NODES = [ + { lat: 30, lon: 18 }, + { lat: -14, lon: 72 }, + { lat: 52, lon: 134 }, + { lat: -36, lon: 205 }, + { lat: 10, lon: 262 }, + { lat: 42, lon: 318 }, + ] + const EDGES: [number, number][] = [ + [0, 1], + [1, 4], + [2, 5], + [0, 5], + [3, 4], + [2, 3], + ] + + const project = (latDeg: number, lonDeg: number, R: number, cx: number, cy: number) => { + const lat = (latDeg * Math.PI) / 180 + const lon = ((lonDeg + angle) * Math.PI) / 180 + const x = Math.cos(lat) * Math.sin(lon) + const y = Math.sin(lat) + const z = Math.cos(lat) * Math.cos(lon) + const y2 = y * Math.cos(tilt) - z * Math.sin(tilt) + const z2 = y * Math.sin(tilt) + z * Math.cos(tilt) + return { px: cx + x * R, py: cy - y2 * R, z: z2 } + } + + const resize = () => { + const r = canvas.getBoundingClientRect() + w = r.width + h = r.height + canvas.width = Math.round(w * dpr) + canvas.height = Math.round(h * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + } + + // stroke a polyline of projected points, alpha shaded by depth per segment + const arc = (pts: { px: number; py: number; z: number }[]) => { + ctx.lineWidth = 1 + for (let i = 1; i < pts.length; i++) { + const a = pts[i - 1] + const b = pts[i] + const z = (a.z + b.z) / 2 + ctx.strokeStyle = `rgba(255,255,255,${z > 0 ? 0.1 + 0.4 * z : 0.05})` + ctx.beginPath() + ctx.moveTo(a.px, a.py) + ctx.lineTo(b.px, b.py) + ctx.stroke() + } + } + + const draw = () => { + ctx.clearRect(0, 0, w, h) + const R = Math.min(w, h) * 0.5 + const cx = w * 0.52 + const cy = h * 0.5 + + // parallels (latitude rings) + for (let la = -80; la <= 80; la += 20) { + const pts = [] + for (let lo = 0; lo <= 360; lo += 10) pts.push(project(la, lo, R, cx, cy)) + arc(pts) + } + // meridians (longitude) + for (let lo = 0; lo < 360; lo += 30) { + const pts = [] + for (let la = -90; la <= 90; la += 9) pts.push(project(la, lo, R, cx, cy)) + arc(pts) + } + + // silhouette + ctx.strokeStyle = 'rgba(255,255,255,0.16)' + ctx.lineWidth = 1.2 + ctx.beginPath() + ctx.arc(cx, cy, R, 0, Math.PI * 2) + ctx.stroke() + + // network edges + const proj = NODES.map((n) => project(n.lat, n.lon, R, cx, cy)) + ctx.lineWidth = 1 + for (const [a, b] of EDGES) { + const pa = proj[a] + const pb = proj[b] + if (pa.z > -0.1 && pb.z > -0.1) { + ctx.strokeStyle = `rgba(255,255,255,${0.12 + 0.22 * Math.min(pa.z, pb.z)})` + ctx.beginPath() + ctx.moveTo(pa.px, pa.py) + ctx.lineTo(pb.px, pb.py) + ctx.stroke() + } + } + + // node markers + pulse + const pulse = (Math.sin(angle * 0.05) + 1) / 2 + proj.forEach((p) => { + if (p.z <= 0) return + ctx.fillStyle = `rgba(255,255,255,${0.55 + 0.45 * p.z})` + ctx.beginPath() + ctx.arc(p.px, p.py, 2.6, 0, Math.PI * 2) + ctx.fill() + ctx.strokeStyle = `rgba(255,255,255,${0.22 * p.z * (1 - pulse)})` + ctx.lineWidth = 1 + ctx.beginPath() + ctx.arc(p.px, p.py, 3 + pulse * 8, 0, Math.PI * 2) + ctx.stroke() + }) + } + + resize() + const ro = new ResizeObserver(resize) + ro.observe(canvas) + const io = new IntersectionObserver((e) => { + visible = e[0]?.isIntersecting ?? true + }) + io.observe(canvas) + + if (reduce) { + draw() + return () => { + ro.disconnect() + io.disconnect() + } + } + + const FRAME = 1000 / 30 + let last = 0 + const loop = (t: number) => { + raf = requestAnimationFrame(loop) + if (!visible) { + last = t + return + } + const dt = t - last + if (dt < FRAME) return + last = t + angle += Math.min(dt, 60) * 0.0045 + draw() + } + raf = requestAnimationFrame(loop) + + return () => { + cancelAnimationFrame(raf) + ro.disconnect() + io.disconnect() + } + }, []) + + return +} diff --git a/src/components/landing/HeroCanvas.tsx b/src/components/landing/HeroCanvas.tsx new file mode 100644 index 0000000..54e31e1 --- /dev/null +++ b/src/components/landing/HeroCanvas.tsx @@ -0,0 +1,146 @@ +import { useEffect, useRef } from 'react' + +/** + * Living candlestick stream behind the hero. Performance-first: NO per-candle + * shadowBlur (the canvas perf killer) — crisp bright candles on black read as neon, + * with a cheap 2-pass glow only on the amber price line. Throttled to ~30fps, + * DPR capped, paused offscreen + on prefers-reduced-motion. + */ +export function HeroCanvas({ className }: { className?: string }) { + const ref = useRef(null) + + useEffect(() => { + const canvas = ref.current + if (!canvas) return + const ctx = canvas.getContext('2d', { alpha: true }) + if (!ctx) return + + const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches + const dpr = Math.min(window.devicePixelRatio || 1, 1.5) + const CW = 16 + let w = 0 + let h = 0 + let raf = 0 + let offset = 0 + let visible = true + + type C = { o: number; h: number; l: number; c: number } + let candles: C[] = [] + let price = 100 + + const next = (): C => { + const o = price + const c = Math.max(20, o + (Math.random() - 0.485) * 3) + const hi = Math.max(o, c) + Math.random() * 2 + const lo = Math.min(o, c) - Math.random() * 2 + price = c + return { o, h: hi, l: lo, c } + } + + const resize = () => { + const r = canvas.getBoundingClientRect() + w = r.width + h = r.height + canvas.width = Math.round(w * dpr) + canvas.height = Math.round(h * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + const need = Math.ceil(w / CW) + 3 + while (candles.length < need) candles.push(next()) + if (candles.length > need) candles = candles.slice(-need) + } + + const draw = () => { + ctx.clearRect(0, 0, w, h) + let min = Infinity + let max = -Infinity + for (const k of candles) { + if (k.l < min) min = k.l + if (k.h > max) max = k.h + } + const pad = (max - min) * 0.18 || 1 + min -= pad + max += pad + const Y = (p: number) => h - ((p - min) / (max - min)) * h + const bw = CW * 0.52 + + ctx.lineWidth = 1.5 + for (let i = 0; i < candles.length; i++) { + const k = candles[i] + const x = i * CW - offset + CW / 2 + const col = k.c >= k.o ? '#2bd974' : '#f2444d' + ctx.strokeStyle = col + ctx.fillStyle = col + ctx.beginPath() + ctx.moveTo(x, Y(k.h)) + ctx.lineTo(x, Y(k.l)) + ctx.stroke() + const yo = Y(k.o) + const yc = Y(k.c) + ctx.fillRect(x - bw / 2, Math.min(yo, yc), bw, Math.max(2, Math.abs(yc - yo))) + } + + // amber "current price" line — cheap glow via wide-faint + thin-bright (no shadowBlur) + const ly = Y(price) + ctx.setLineDash([2, 7]) + ctx.strokeStyle = 'rgba(255,176,32,0.18)' + ctx.lineWidth = 6 + ctx.beginPath() + ctx.moveTo(0, ly) + ctx.lineTo(w, ly) + ctx.stroke() + ctx.strokeStyle = 'rgba(255,176,32,0.9)' + ctx.lineWidth = 1.5 + ctx.beginPath() + ctx.moveTo(0, ly) + ctx.lineTo(w, ly) + ctx.stroke() + ctx.setLineDash([]) + } + + resize() + const ro = new ResizeObserver(resize) + ro.observe(canvas) + + const io = new IntersectionObserver((e) => { + visible = e[0]?.isIntersecting ?? true + }) + io.observe(canvas) + + if (reduce) { + draw() + return () => { + ro.disconnect() + io.disconnect() + } + } + + const FRAME = 1000 / 30 // throttle to ~30fps + let last = 0 + const loop = (t: number) => { + raf = requestAnimationFrame(loop) + if (!visible) { + last = t + return + } + const dt = t - last + if (dt < FRAME) return + last = t + offset += Math.min(dt, 60) * 0.024 + if (offset >= CW) { + offset -= CW + candles.push(next()) + candles.shift() + } + draw() + } + raf = requestAnimationFrame(loop) + + return () => { + cancelAnimationFrame(raf) + ro.disconnect() + io.disconnect() + } + }, []) + + return +} diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..d819a5d --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,54 @@ +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { Plus } from 'lucide-react' +import { cn } from '@/lib/utils' + +export const Accordion = AccordionPrimitive.Root + +export const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = 'AccordionItem' + +export const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + +)) +AccordionTrigger.displayName = 'AccordionTrigger' + +export const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
+ {children} +
+
+)) +AccordionContent.displayName = 'AccordionContent' diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..04668a0 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,38 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center gap-1.5 font-mono text-[0.65rem] uppercase tracking-[0.14em] px-2 py-0.5 border', + { + variants: { + tone: { + lime: 'border-lime/50 text-lime bg-lime/[0.08]', + amber: 'border-bone/30 text-bone bg-bone/[0.04]', + coral: 'border-coral/50 text-coral bg-coral/[0.08]', + muted: 'border-line text-muted bg-transparent', + }, + }, + defaultVariants: { tone: 'muted' }, + }, +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps { + dot?: boolean +} + +export function Badge({ className, tone, dot, children, ...props }: BadgeProps) { + return ( + + {dot && ( + + + + + )} + {children} + + ) +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..648fc54 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap font-mono font-bold uppercase tracking-[0.08em] transition-colors duration-150 select-none cursor-pointer disabled:pointer-events-none disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bone focus-visible:ring-offset-2 focus-visible:ring-offset-paper', + { + variants: { + variant: { + // Swiss: solid ink button, inverts to outline-on-hover + primary: 'bg-bone text-ink border border-bone hover:bg-paper hover:text-bone', + // green LONG-signal button + amber: 'bg-lime text-ink border border-lime hover:bg-paper hover:text-lime', + outline: 'border border-bone text-bone hover:bg-bone hover:text-ink', + ghost: 'text-muted hover:text-bone hover:bg-bone/[0.06]', + link: 'text-bone underline underline-offset-4 decoration-1 hover:decoration-2 p-0 h-auto', + }, + size: { + sm: 'h-9 px-4 text-[0.7rem]', + md: 'h-11 px-6 text-xs', + lg: 'h-14 px-8 text-sm', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { variant: 'primary', size: 'md' }, + }, +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +export const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + }, +) +Button.displayName = 'Button' + +export { buttonVariants } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..9e11314 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,34 @@ +import * as React from 'react' +import * as DialogPrimitive from '@radix-ui/react-dialog' +import { X } from 'lucide-react' +import { cn } from '@/lib/utils' + +export const Dialog = DialogPrimitive.Root +export const DialogTrigger = DialogPrimitive.Trigger +export const DialogClose = DialogPrimitive.Close + +export const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + + + +)) +DialogContent.displayName = 'DialogContent' + +export const DialogTitle = DialogPrimitive.Title +export const DialogDescription = DialogPrimitive.Description diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..b995c47 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,16 @@ +import { cn } from '@/lib/utils' + +/** Shimmer placeholder for live cards while data loads. */ +export function Skeleton({ className, ...props }: React.HTMLAttributes) { + return ( +
+ ) +} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..c394a93 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import * as TabsPrimitive from '@radix-ui/react-tabs' +import { cn } from '@/lib/utils' + +export const Tabs = TabsPrimitive.Root + +export const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = 'TabsList' + +export const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = 'TabsTrigger' + +export const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = 'TabsContent' diff --git a/src/components/viz/AgentWorkflow.tsx b/src/components/viz/AgentWorkflow.tsx new file mode 100644 index 0000000..cf34482 --- /dev/null +++ b/src/components/viz/AgentWorkflow.tsx @@ -0,0 +1,396 @@ +import { useEffect, useState } from 'react' +import type { CoinScreen } from '@/hooks/useHyperliquid' +import type { Direction } from '@/lib/signals' +import { fmtPrice, fmtCompact } from '@/lib/format' + +/** + * Agent workflow — the pipeline as a spatial directed graph. Real market data + * enters at the left (Market → TA engine), fans out to the 4 committee agents, + * converges through the Risk manager into the Verdict (the call), then flows on + * to paper Execution and Telegram. Data "packets" animate edge-by-edge so you + * watch information pass from one agent to the next, like a workflow canvas. + * + * Every node is clickable → a detail panel (its role + live output). Hovering a + * node traces its connected path and cross-links the Orchestra + Committee. The + * 4 agents stay in sync with `activeAgent`. Honest Carbon-Desk palette: amber is + * only "the call" / selection; green/red only direction; structure stays carbon. + */ + +const sign = (n: number) => (n >= 0 ? '+' : '') +const dirColor = (d?: Direction) => + d === 'LONG' ? 'var(--color-long)' : d === 'SHORT' ? 'var(--color-short)' : 'var(--color-muted)' + +const BONE = 'var(--color-bone)' +const MUTED = 'var(--color-muted)' +const FAINT = 'var(--color-faint)' +const ACCENT = 'var(--color-accent)' + +const AGENT_KEYS = ['TrendAgent', 'BreakoutAgent', 'MeanReversionAgent', 'FundingAgent'] + +const AGENT_RULE: Record = { + TrendAgent: 'LONG when EMA9 > EMA21 and RSI > 50; SHORT on the reverse; else SKIP.', + BreakoutAgent: 'LONG when price pushes the upper Bollinger band (%B > 0.92); SHORT at the lower (< 0.08).', + MeanReversionAgent: 'Fade extremes — LONG when RSI < 32 in the lower band; SHORT when RSI > 68 at the upper.', + FundingAgent: 'Fade crowded funding — SHORT when funding-Z > 1.5; LONG when < −1.5.', +} + +/* ── graph model (viewBox space 1000 × 300) ───────────────────────────────── */ + +const VB_W = 1000 +const VB_H = 300 + +type Kind = 'data' | 'agent' | 'risk' | 'call' | 'paper' | 'comms' +type GNode = { key: string; label: string; code: string; cx: number; cy: number; w: number; h: number; kind: Kind } + +const NODES: GNode[] = [ + { key: 'market', label: 'Market', code: 'L1', cx: 70, cy: 150, w: 96, h: 46, kind: 'data' }, + { key: 'ta', label: 'TA engine', code: 'L2', cx: 198, cy: 150, w: 114, h: 46, kind: 'data' }, + { key: 'TrendAgent', label: 'Trend', code: 'L3', cx: 366, cy: 50, w: 132, h: 46, kind: 'agent' }, + { key: 'BreakoutAgent', label: 'Breakout', code: 'L3', cx: 366, cy: 116, w: 132, h: 46, kind: 'agent' }, + { key: 'MeanReversionAgent', label: 'Mean-Rev', code: 'L3', cx: 366, cy: 182, w: 132, h: 46, kind: 'agent' }, + { key: 'FundingAgent', label: 'Funding', code: 'L3', cx: 366, cy: 248, w: 132, h: 46, kind: 'agent' }, + { key: 'risk', label: 'Risk mgr', code: 'L3', cx: 544, cy: 150, w: 112, h: 46, kind: 'risk' }, + { key: 'verdict', label: 'Verdict', code: 'L4', cx: 698, cy: 150, w: 128, h: 58, kind: 'call' }, + { key: 'exec', label: 'Execution', code: 'L5', cx: 846, cy: 150, w: 112, h: 46, kind: 'paper' }, + { key: 'telegram', label: 'Telegram', code: 'L6', cx: 958, cy: 150, w: 90, h: 46, kind: 'comms' }, +] +const NODE = Object.fromEntries(NODES.map((n) => [n.key, n])) as Record + +type EdgeTone = 'data' | 'agent' | 'call' | 'comms' +type GEdge = { from: string; to: string; tone: EdgeTone; agent?: string } +const EDGES: GEdge[] = [ + { from: 'market', to: 'ta', tone: 'data' }, + { from: 'ta', to: 'TrendAgent', tone: 'data' }, + { from: 'ta', to: 'BreakoutAgent', tone: 'data' }, + { from: 'ta', to: 'MeanReversionAgent', tone: 'data' }, + { from: 'ta', to: 'FundingAgent', tone: 'data' }, + { from: 'TrendAgent', to: 'risk', tone: 'agent', agent: 'TrendAgent' }, + { from: 'BreakoutAgent', to: 'risk', tone: 'agent', agent: 'BreakoutAgent' }, + { from: 'MeanReversionAgent', to: 'risk', tone: 'agent', agent: 'MeanReversionAgent' }, + { from: 'FundingAgent', to: 'risk', tone: 'agent', agent: 'FundingAgent' }, + { from: 'risk', to: 'verdict', tone: 'data' }, + { from: 'verdict', to: 'exec', tone: 'call' }, + { from: 'exec', to: 'telegram', tone: 'comms' }, +] + +const COL: Record = { market: 0, ta: 1, TrendAgent: 2, BreakoutAgent: 2, MeanReversionAgent: 2, FundingAgent: 2, risk: 3, verdict: 4, exec: 5, telegram: 6 } + +function edgePath(a: GNode, b: GNode) { + const x1 = a.cx + a.w / 2 + const y1 = a.cy + const x2 = b.cx - b.w / 2 + const y2 = b.cy + const dx = Math.max(26, (x2 - x1) * 0.5) + return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}` +} + +function useReducedMotion() { + const [reduced, setReduced] = useState(false) + useEffect(() => { + const m = window.matchMedia('(prefers-reduced-motion: reduce)') + const on = () => setReduced(m.matches) + on() + m.addEventListener?.('change', on) + return () => m.removeEventListener?.('change', on) + }, []) + return reduced +} + +/* ── live detail per node ─────────────────────────────────────────────────── */ + +type Detail = { code: string; title: string; tag: string; tagColor: string; role: string; output: string } + +function detailFor(key: string, d: CoinScreen | null, decision: Direction): Detail | null { + const node = NODE[key] + if (!node) return null + const ta = d?.ta + const stat = d?.stat + const v = d?.verdict + if (AGENT_KEYS.includes(key)) { + const vote = d?.votes.find((x) => x.agent === key) + const dir = vote?.direction ?? 'SKIP' + return { + code: 'L3 · SIGNAL', title: node.label, tag: dir === 'SKIP' ? 'skip' : dir, tagColor: dirColor(dir), + role: AGENT_RULE[key] ?? '', output: vote ? `${dir} · score ${(vote.score * 100).toFixed(0)} — ${vote.reason}` : '—', + } + } + switch (key) { + case 'market': + return { code: 'L1 · DATA', title: 'Market data', tag: 'live', tagColor: BONE, + role: 'Polls OHLCV candles, mark price, funding & open interest from the Hyperliquid public API.', + output: stat ? `mark $${fmtPrice(stat.mark)} · 24h ${sign(ta?.changePct ?? 0)}${(ta?.changePct ?? 0).toFixed(2)}% · OI $${fmtCompact(stat.oiUsd)}` : 'connecting…' } + case 'ta': + return { code: 'L2 · ANALYSIS', title: 'TA engine', tag: 'calc', tagColor: MUTED, + role: 'Recomputes RSI / MACD / EMA / ATR / Bollinger / funding-Z in-browser on every candle.', + output: ta ? `RSI ${ta.rsi.toFixed(1)} · %B ${ta.pctB.toFixed(2)} · ATR ${ta.atrPct.toFixed(2)}% · funding-Z ${ta.fundingZ.toFixed(2)}` : 'connecting…' } + case 'risk': + return { code: 'L3 · RISK', title: 'Risk manager', tag: 'calc', tagColor: MUTED, + role: 'Caps size, drawdown & correlation — confidence-floor gate plus one position per coin.', + output: v ? `gate ${v.confidence >= 0.55 ? 'open' : 'hold'} · conf floor 0.55 · 1 position / coin` : '—' } + case 'verdict': + return { code: 'L4 · DECISION', title: 'Screener — the call', tag: decision, tagColor: dirColor(decision), + role: 'Aggregates the committee votes into the final decision: confidence, size, TP and SL (deterministic).', + output: v ? `${v.decision} · conf ${(v.confidence * 100).toFixed(0)}% · size ${v.sizePct.toFixed(1)}% · TP +${v.tpPct.toFixed(1)}% / SL -${Math.abs(v.slPct).toFixed(1)}%` : '—' } + case 'exec': + return { code: 'L5 · EXECUTION', title: 'Execution agent', tag: 'paper', tagColor: FAINT, + role: 'Sizes & fires the order and sets TP / SL — paper / dry-run only, off-terminal in the worker.', + output: v ? (v.decision === 'SKIP' ? 'no position · SKIP (paper / dry-run)' : `paper · ${v.decision} ${v.sizePct.toFixed(1)}% · TP +${v.tpPct.toFixed(1)}% / SL -${Math.abs(v.slPct).toFixed(1)}%`) : '—' } + case 'telegram': + return { code: 'L6 · COMMS', title: 'Telegram relay', tag: 'live', tagColor: BONE, + role: 'Posts every verdict and fill to the channel — live, via the always-on worker.', + output: v ? `verdict ${v.decision} relayed to channel` : '—' } + default: + return null + } +} + +/* ── node visual derivation ───────────────────────────────────────────────── */ + +function nodeVisual(n: GNode, d: CoinScreen | null, decision: Direction) { + if (n.kind === 'agent') { + const vote = d?.votes.find((x) => x.agent === n.key) + const dir = vote?.direction ?? 'SKIP' + return { dot: dirColor(dir), tag: dir === 'SKIP' ? 'SKIP' : dir, tagColor: dirColor(dir), nameColor: BONE, dim: false } + } + switch (n.kind) { + case 'data': + return { dot: BONE, tag: n.key === 'ta' ? 'CALC' : 'LIVE', tagColor: FAINT, nameColor: BONE, dim: false } + case 'risk': + return { dot: MUTED, tag: 'CALC', tagColor: FAINT, nameColor: 'var(--color-bone-dim)', dim: false } + case 'call': + return { dot: dirColor(decision), tag: decision, tagColor: dirColor(decision), nameColor: dirColor(decision), dim: false } + case 'paper': + return { dot: FAINT, tag: 'PAPER', tagColor: FAINT, nameColor: 'var(--color-bone-dim)', dim: true } + case 'comms': + return { dot: BONE, tag: 'LIVE', tagColor: FAINT, nameColor: BONE, dim: false } + default: + return { dot: BONE, tag: '', tagColor: FAINT, nameColor: BONE, dim: false } + } +} + +/* ── component ────────────────────────────────────────────────────────────── */ + +export function AgentWorkflow({ + data, + decision, + activeAgent, + onSelect, + onHover, + onInspect, + updatedAt, + live, + loading, +}: { + data: CoinScreen | null + decision: Direction + activeAgent?: string | null + onSelect?: (a: string | null) => void + onHover?: (a: string | null) => void + onInspect?: (s: string | null) => void + updatedAt: number + live: boolean + loading?: boolean +}) { + const reduced = useReducedMotion() + const [hoverKey, setHoverKey] = useState(null) + const [selKey, setSelKey] = useState(null) + + // committee selection (activeAgent) also lights / details the matching agent node + const externalAgent = activeAgent && AGENT_KEYS.includes(activeAgent) ? activeAgent : null + const litKey = hoverKey ?? selKey ?? externalAgent + const detailKey = selKey ?? externalAgent + const detail = detailKey ? detailFor(detailKey, data, decision) : null + + const onNode = (key: string) => { + const next = selKey === key ? null : key + setSelKey(next) + // keep the committee in one shared state: agent node pins that agent, any other node clears it + onSelect?.(AGENT_KEYS.includes(key) ? next : null) + } + const onEnter = (key: string) => { + setHoverKey(key) + if (AGENT_KEYS.includes(key)) onHover?.(key) + const dt = detailFor(key, data, decision) + if (dt) onInspect?.(`${dt.code} · ${dt.title} — ${dt.output}`) + } + const onLeave = () => { + setHoverKey(null) + onHover?.(null) + onInspect?.(null) + } + const onKey = (e: React.KeyboardEvent, key: string) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onNode(key) + } + } + + const edgeColor = (e: GEdge) => { + if (e.tone === 'call') return ACCENT + if (e.tone === 'agent') return dirColor(data?.votes.find((v) => v.agent === e.agent)?.direction) + if (e.tone === 'comms') return BONE + return 'rgba(232, 234, 237, 0.45)' + } + + return ( +
+
+
+ Agent flow + click a node · hover to trace the path +
+ + + {live ? 'flowing' : updatedAt > 0 ? 'paused' : 'connecting'} + +
+ +
+
+ + {/* edges */} + + {EDGES.map((e) => { + const a = NODE[e.from] + const b = NODE[e.to] + const d = edgePath(a, b) + const col = edgeColor(e) + const lit = litKey != null && (e.from === litKey || e.to === litKey) + const dimmed = litKey != null && !lit + return ( + + + {/* arrowhead at the target end (edges arrive ~horizontally) */} + + + ) + })} + + + {/* animated data packets (SMIL — scales with viewBox; off under reduced-motion) */} + {!reduced && !loading && ( + + {EDGES.map((e) => { + const a = NODE[e.from] + const b = NODE[e.to] + const d = edgePath(a, b) + const col = edgeColor(e) + const lit = litKey != null && (e.from === litKey || e.to === litKey) + const dimmed = litKey != null && !lit + if (dimmed) return null + const dur = lit ? 1.5 : 2.6 + const base = (COL[e.from] ?? 0) * 0.16 + return [0, dur / 2].map((off, k) => ( + + + + + )) + })} + + )} + + {/* nodes */} + {NODES.map((n) => { + const vis = nodeVisual(n, data, decision) + const on = detailKey === n.key + const hov = hoverKey === n.key + const x = n.cx - n.w / 2 + const y = n.cy - n.h / 2 + const stroke = on ? ACCENT : hov ? 'var(--color-line-strong)' : 'var(--color-line-soft)' + const fill = on || hov ? 'var(--color-panel-2)' : 'var(--color-inset)' + const padL = x + 13 + const isCall = n.kind === 'call' + return ( + onNode(n.key)} + onKeyDown={(ev) => onKey(ev, n.key)} + onMouseEnter={() => onEnter(n.key)} + onMouseLeave={onLeave} + style={{ opacity: vis.dim && !on && !hov ? 0.62 : 1, outline: 'none' }} + > + + {/* top row: status dot + layer code (left) · tag (right) */} + + + {n.code} + + {isCall ? ( + <> + + {data?.verdict ? `${(data.verdict.confidence * 100).toFixed(0)}%` : 'VERDICT'} + + + {vis.tag} + + + ) : ( + <> + + {vis.tag} + + + {n.label} + + + )} + + ) + })} + +
+
+ + {/* detail strip — updates on every node click */} +
+ {detail ? ( +
+
+ {detail.code} + {detail.title} + + {detail.tag} + + +
+

{detail.role}

+

↳ {detail.output}

+
+ ) : ( +

+ Click any node to inspect that agent — its role and live output. Hover to trace the data path. +

+ )} +
+
+ ) +} diff --git a/src/components/viz/ConfidenceRing.tsx b/src/components/viz/ConfidenceRing.tsx new file mode 100644 index 0000000..85dac5f --- /dev/null +++ b/src/components/viz/ConfidenceRing.tsx @@ -0,0 +1,57 @@ +import { clamp } from '@/lib/seed' + +type Props = { + value: number // 0..1 + size?: number + tone?: string + label?: string +} + +/** Circular confidence gauge. */ +export function ConfidenceRing({ value, size = 84, tone = 'var(--color-lime)', label = 'CONF' }: Props) { + const v = clamp(value, 0, 1) + const stroke = 6 + const r = (size - stroke - 2) / 2 + const c = 2 * Math.PI * r + const off = c * (1 - v) + const cx = size / 2 + return ( + + + + + {Math.round(v * 100)} + + + {label} + + + ) +} diff --git a/src/components/viz/LiveFeed.tsx b/src/components/viz/LiveFeed.tsx new file mode 100644 index 0000000..7883777 --- /dev/null +++ b/src/components/viz/LiveFeed.tsx @@ -0,0 +1,177 @@ +import { useEffect, useRef, useState } from 'react' +import type { CoinScreen } from '@/hooks/useHyperliquid' +import type { Direction } from '@/lib/signals' +import { fmtPrice, fmtCompact } from '@/lib/format' + +/** + * Live feed — the agent chain streaming in real time. On every poll the whole + * pipeline re-runs and each layer's output cascades into the tape L1 → L6: + * data ingest → TA → the 4 agent votes → verdict → paper execution → comms. + * Every line is built from REAL screen data and carries an honest status tag + * (live / calc / vote / the call / paper). New rows flash + slide in, so you + * literally watch the agents receive the new candle and react. + */ + +type Tone = 'live' | 'calc' | 'vote' | 'call' | 'paper' +type FeedEvent = { code: string; agent: string; msg: string; tag: string; tone: Tone; dir?: Direction } +type Row = FeedEvent & { id: string; ts: number; coin: string; fresh: boolean; i: number } + +const sign = (n: number) => (n >= 0 ? '+' : '') +const hhmmss = (ts: number) => new Date(ts).toISOString().slice(11, 19) +const shortAgent = (a: string) => + a.replace('Agent', '').replace('MeanReversion', 'Mean-Rev') + +const dirColor = (d?: Direction) => + d === 'LONG' ? 'var(--color-long)' : d === 'SHORT' ? 'var(--color-short)' : 'var(--color-muted)' + +function toneDot(e: FeedEvent): string { + switch (e.tone) { + case 'vote': return dirColor(e.dir) + case 'call': return 'var(--color-accent)' + case 'live': return 'var(--color-bone)' + case 'calc': return 'var(--color-muted)' + default: return 'var(--color-faint)' + } +} +function toneTag(e: FeedEvent): string { + if (e.tone === 'vote') return dirColor(e.dir) + if (e.tone === 'call') return 'var(--color-accent)' + return 'var(--color-faint)' +} + +/** Build one batch of feed events for a poll — pure, from real data, L1 → L6. */ +function buildBatch(d: CoinScreen, tf: string): FeedEvent[] { + const { candles, ta, stat, votes, verdict } = d + const TF = tf.toUpperCase() + const out: FeedEvent[] = [] + const lastC = candles[candles.length - 1] + + // L1 · data ingest + out.push({ code: 'L1', agent: 'Candle', tone: 'live', tag: 'live', msg: `+${candles.length} ${TF} candles · close $${fmtPrice(lastC?.c ?? ta.price)}` }) + out.push({ code: 'L1', agent: 'Ticker', tone: 'live', tag: 'live', msg: `mark $${fmtPrice(stat?.mark ?? ta.price)} · 24h ${sign(ta.changePct)}${ta.changePct.toFixed(2)}%` }) + if (stat) out.push({ code: 'L1', agent: 'Funding·OI', tone: 'live', tag: 'live', msg: `funding ${sign(stat.funding)}${(stat.funding * 100).toFixed(4)}% · OI $${fmtCompact(stat.oiUsd)}` }) + + // L2 · analysis + out.push({ + code: 'L2', agent: 'TA engine', tone: 'calc', tag: 'calc', + msg: `RSI ${ta.rsi.toFixed(1)} · MACD ${ta.macdHist >= 0 ? '▲' : '▼'} · EMA9 ${ta.ema9 >= ta.ema21 ? '>' : '<'} EMA21 · ATR ${ta.atrPct.toFixed(2)}% · %B ${ta.pctB.toFixed(2)}`, + }) + + // L3 · the 4 agent votes (real reasons) + for (const v of votes) { + out.push({ code: 'L3', agent: shortAgent(v.agent), tone: 'vote', tag: v.direction === 'SKIP' ? 'skip' : v.direction, dir: v.direction, msg: v.reason }) + } + + // L4 · decision (the call) + out.push({ + code: 'L4', agent: 'Screener', tone: 'call', tag: verdict.decision, dir: verdict.decision, + msg: `verdict ${verdict.decision} · conf ${(verdict.confidence * 100).toFixed(0)}% · net ${sign(verdict.net)}${verdict.net.toFixed(2)} · size ${verdict.sizePct.toFixed(1)}%`, + }) + + // L5 · execution (paper / dry-run — honest) + out.push({ + code: 'L5', agent: 'Execution', tone: 'paper', tag: 'paper', + msg: verdict.decision === 'SKIP' + ? 'no position · SKIP (paper / dry-run)' + : `paper · ${verdict.decision} ${verdict.sizePct.toFixed(1)}% · TP +${verdict.tpPct.toFixed(1)}% / SL -${Math.abs(verdict.slPct).toFixed(1)}%`, + }) + + // L6 · comms + out.push({ code: 'L6', agent: 'Telegram', tone: 'live', tag: 'live', msg: `verdict ${verdict.decision} relayed to channel (worker)` }) + + return out +} + +export function LiveFeed({ + data, + coin, + tf, + updatedAt, + live, + loading, + onInspect, +}: { + data: CoinScreen | null + coin: string + tf: string + updatedAt: number + live: boolean + loading?: boolean + onInspect?: (s: string | null) => void +}) { + const [rows, setRows] = useState([]) + const lastTs = useRef(0) + const seq = useRef(0) + + useEffect(() => { + if (!data || !updatedAt || updatedAt === lastTs.current) return + lastTs.current = updatedAt + const bid = ++seq.current + const batch = buildBatch(data, tf).map((e, i) => ({ + ...e, + id: `${bid}-${i}`, + ts: updatedAt, + coin, + fresh: true, + i, + })) + setRows((prev) => { + const aged = prev.length ? prev.map((r) => (r.fresh ? { ...r, fresh: false } : r)) : prev + return [...batch, ...aged].slice(0, 60) + }) + }, [updatedAt, data, coin, tf]) + + return ( +
+
+
+ Live feed + data → analysis → signal → decision → execution → comms +
+ + + {live ? 'streaming' : updatedAt > 0 ? 'paused' : 'connecting'} + +
+ +
+ {loading || rows.length === 0 ? ( +
+ {[0, 1, 2, 3, 4, 5].map((i) => ( +
+
+
+
+
+ ))} +
+ ) : ( + rows.map((r) => { + const dot = toneDot(r) + return ( +
onInspect?.(`${r.code} · ${r.agent} — ${r.msg}`)} + onMouseLeave={() => onInspect?.(null)} + > + {hhmmss(r.ts)} + {r.code} + + {r.agent} + {r.msg} + {r.coin} + {r.tag} +
+ ) + }) + )} +
+
+ ) +} diff --git a/src/components/viz/Orchestra.tsx b/src/components/viz/Orchestra.tsx new file mode 100644 index 0000000..bcb6c67 --- /dev/null +++ b/src/components/viz/Orchestra.tsx @@ -0,0 +1,239 @@ +import { cn } from '@/lib/utils' +import type { Direction, Vote } from '@/lib/signals' + +/** + * The orchestra — the full 6-layer agent pipeline as a clean layered flow. + * Data → Analysis → Signal → Decision → Execution → Comms. Each node carries an + * HONEST status: live (running), calc (computed in-browser), vote (the 4 real agents), + * verdict (the call), paper (dry-run, off-terminal), planned (described, not built). + * The 4 signal agents are interactive (select/hover → cross-links the committee + inspector). + */ + +type Status = 'live' | 'calc' | 'paper' | 'planned' + +type Node = { label: string; status: Status; desc: string; agent?: string } +type Layer = { code: string; name: string; nodes: Node[] } + +const STATIC: Layer[] = [ + { + code: 'L1', + name: 'Data', + nodes: [ + { label: 'Candle', status: 'live', desc: 'L1 · CandleAgent — polls OHLCV candles from Hyperliquid.' }, + { label: 'Ticker', status: 'live', desc: 'L1 · TickerAgent — live mark / mid price.' }, + { label: 'Funding·OI', status: 'live', desc: 'L1 · FundingOIAgent — funding rate & open interest.' }, + { label: 'Orderbook', status: 'planned', desc: 'L1 · OrderbookAgent — top-of-book depth (planned).' }, + { label: 'Liquidations', status: 'planned', desc: 'L1 · LiquidationsAgent — liquidation stream (planned).' }, + ], + }, + { + code: 'L2', + name: 'Analysis', + nodes: [ + { label: 'TA engine', status: 'live', desc: 'L2 · recomputes RSI / MACD / EMA / ATR / Bollinger / funding-Z in-browser.' }, + { label: 'Volatility', status: 'calc', desc: 'L2 · VolatilityAgent — ATR regime & volume Δ (computed).' }, + ], + }, + // L3 (signal) nodes are injected from live votes; risk is static + { code: 'L3', name: 'Signal', nodes: [] }, + { + code: 'L4', + name: 'Decision', + nodes: [ + { label: 'Screener', status: 'live', desc: 'L4 · aggregates the votes into the verdict — confidence, size, TP, SL (deterministic; LLM planned).' }, + { label: 'Conf gate', status: 'calc', desc: 'L4 · ConfidenceGate — forces SKIP below the confidence floor.' }, + ], + }, + { + code: 'L5', + name: 'Execution', + nodes: [ + { label: 'Execution', status: 'paper', desc: 'L5 · ExecutionAgent — sizes & fires the order, sets TP/SL (paper / dry-run, off-terminal).' }, + { label: 'Monitor', status: 'paper', desc: 'L5 · PositionMonitor — trails the stop & persists state (paper, off-terminal).' }, + ], + }, + { + code: 'L6', + name: 'Comms', + nodes: [ + { label: 'Telegram', status: 'live', desc: 'L6 · posts every verdict & fill to Telegram (live, via the worker).' }, + { label: '/learn', status: 'planned', desc: 'L6 · retrospective over closed trades (planned; LLM).' }, + ], + }, +] + +const RISK: Node = { label: 'Risk mgr', status: 'calc', desc: 'L3 · RiskManager — caps size, drawdown & correlation (conf-floor + 1 position/coin).' } + +const dirColor = (d: Direction) => + d === 'LONG' ? 'var(--color-long)' : d === 'SHORT' ? 'var(--color-short)' : 'var(--color-muted)' + +const AGENT_RULE: Record = { + TrendAgent: 'L3 · Trend — LONG when EMA9 > EMA21 & RSI > 50; SHORT on the reverse.', + BreakoutAgent: 'L3 · Breakout — LONG when %B > 0.92 (upper band); SHORT when %B < 0.08.', + MeanReversionAgent: 'L3 · Mean-Rev — fade extremes: LONG RSI < 32 lower band; SHORT RSI > 68 upper.', + FundingAgent: 'L3 · Funding — fade crowded funding: SHORT funding-Z > 1.5; LONG < −1.5.', +} + +// status → dot + tag (never colour-only: every node carries a text tag too) +const STATUS_TAG: Record = { live: 'live', calc: 'calc', paper: 'paper', planned: 'soon' } +const dotFor = (s: Status) => + s === 'live' ? 'var(--color-bone)' : s === 'calc' ? 'var(--color-muted)' : 'var(--color-faint)' + +function Chip({ + dot, + name, + nameTone, + tag, + tagTone, + dim, + outline, + selectable, + selected, + onClick, + onEnter, + onLeave, +}: { + dot: string + name: string + nameTone?: string + tag: string + tagTone?: string + dim?: boolean + outline?: boolean + selectable?: boolean + selected?: boolean + onClick?: () => void + onEnter?: () => void + onLeave?: () => void +}) { + const Comp = selectable ? 'button' : 'div' + return ( + + + + {name} + + + {tag} + + + ) +} + +export function Orchestra({ + votes, + decision, + activeAgent, + onSelect, + onHover, + onInspect, + loading, +}: { + votes: Vote[] + decision: Direction + activeAgent?: string | null + onSelect?: (a: string | null) => void + onHover?: (a: string | null) => void + onInspect?: (s: string | null) => void + loading?: boolean +}) { + const decCol = dirColor(decision) + + return ( +
+
+ Orchestra + + data → analysis → signal → decision → execution → comms + +
+ +
+ {STATIC.map((layer) => ( +
+
+ {layer.code} + {layer.name} +
+ + {layer.code === 'L3' ? ( + <> + {loading + ? [0, 1, 2, 3].map((i) =>
) + : votes.slice(0, 4).map((v) => { + const c = dirColor(v.direction) + const on = activeAgent === v.agent + const meta = AGENT_RULE[v.agent] ?? `L3 · ${v.agent}` + return ( + onSelect?.(on ? null : v.agent)} + onEnter={() => { + onHover?.(v.agent) + onInspect?.(meta) + }} + onLeave={() => { + onHover?.(null) + onInspect?.(null) + }} + /> + ) + })} + onInspect?.(RISK.desc)} onLeave={() => onInspect?.(null)} /> + + ) : ( + layer.nodes.map((n) => { + const isVerdict = layer.code === 'L4' && n.label === 'Screener' + return ( + onInspect?.(n.desc)} + onLeave={() => onInspect?.(null)} + /> + ) + }) + )} +
+ ))} +
+ + {/* legend (status is never colour-only) */} +
+ live + computed + the call + paper / planned +
+
+ ) +} diff --git a/src/components/viz/TVChart.tsx b/src/components/viz/TVChart.tsx new file mode 100644 index 0000000..dec5daf --- /dev/null +++ b/src/components/viz/TVChart.tsx @@ -0,0 +1,116 @@ +import { useEffect, useRef } from 'react' +import { + createChart, + CandlestickSeries, + ColorType, + CrosshairMode, + type IChartApi, + type ISeriesApi, + type UTCTimestamp, +} from 'lightweight-charts' +import type { Candle } from '@/lib/hyperliquid' + +/** + * Price chart powered by TradingView's Lightweight Charts, fed with our real + * Hyperliquid candles. Professional candlesticks with built-in zoom / pan / + * crosshair. Carbon Desk themed. Re-fits only on instrument/timeframe change so + * a user's zoom survives the live poll. + */ +export function TVChart({ candles, height = 540, fitKey }: { candles: Candle[]; height?: number; fitKey: string }) { + const wrapRef = useRef(null) + const chartRef = useRef(null) + const seriesRef = useRef | null>(null) + const fitKeyRef = useRef(fitKey) + const fittedRef = useRef('') + fitKeyRef.current = fitKey + + useEffect(() => { + const el = wrapRef.current + if (!el) return + const chart = createChart(el, { + autoSize: true, + layout: { + background: { type: ColorType.Solid, color: 'transparent' }, + textColor: '#828a94', + fontFamily: "'Geist Mono', ui-monospace, monospace", + fontSize: 11, + attributionLogo: false, + }, + grid: { + vertLines: { color: 'rgba(255,255,255,0.04)' }, + horzLines: { color: 'rgba(255,255,255,0.05)' }, + }, + rightPriceScale: { borderColor: 'rgba(255,255,255,0.08)' }, + timeScale: { borderColor: 'rgba(255,255,255,0.08)', timeVisible: true, secondsVisible: false }, + crosshair: { + mode: CrosshairMode.Normal, + vertLine: { color: 'rgba(255,176,32,0.5)', width: 1, style: 3, labelBackgroundColor: '#181b20' }, + horzLine: { color: 'rgba(255,176,32,0.5)', width: 1, style: 3, labelBackgroundColor: '#181b20' }, + }, + }) + const series = chart.addSeries(CandlestickSeries, { + upColor: '#2bd974', + downColor: '#f2444d', + borderVisible: false, + wickUpColor: '#2bd974', + wickDownColor: '#f2444d', + }) + chartRef.current = chart + seriesRef.current = series + return () => { + chart.remove() + chartRef.current = null + seriesRef.current = null + } + }, []) + + // Replace data on every poll (preserves the user's zoom). Auto-fit ONCE per + // instrument/timeframe, deferred to a frame so autoSize has measured the + // container first — otherwise the candles bunch up on the right edge. + useEffect(() => { + const series = seriesRef.current + if (!series || candles.length < 2) return + series.setData( + candles.map((c) => ({ + time: Math.floor(c.t / 1000) as UTCTimestamp, + open: c.o, + high: c.h, + low: c.l, + close: c.c, + })), + ) + if (fittedRef.current !== fitKeyRef.current) { + fittedRef.current = fitKeyRef.current + const id = requestAnimationFrame(() => chartRef.current?.timeScale().fitContent()) + return () => cancelAnimationFrame(id) + } + }, [candles]) + + return
+} + +// deterministic faux-candle heights (%) for the connecting skeleton — no random, no real data +const SKELETON_BARS = Array.from({ length: 60 }, (_, i) => { + const h = 48 + 26 * Math.sin(i * 0.55) + 14 * Math.sin(i * 1.7 + 1) + 8 * Math.sin(i * 0.21) + return Math.max(14, Math.min(90, h)) +}) + +/** Chart placeholder shown until live data for the instrument arrives. */ +export function ChartSkeleton({ height = 540 }: { height?: number }) { + return ( +
+
+ {SKELETON_BARS.map((h, i) => ( +
+ ))} +
+
+ + + Connecting · Hyperliquid + +
+
+ ) +} + diff --git a/src/data/seedMarket.ts b/src/data/seedMarket.ts new file mode 100644 index 0000000..698a88b --- /dev/null +++ b/src/data/seedMarket.ts @@ -0,0 +1,63 @@ +/** + * Deterministic seed market data. Renders instantly so cards never hang on a + * skeleton (slow network / backgrounded tab), then live Hyperliquid data replaces + * it. Seed is labelled non-live in the UI. + */ +import type { Candle, MarketStat } from '@/lib/hyperliquid' +import { mulberry32, hashStr, clamp } from '@/lib/seed' + +const BASES: Record = { + BTC: { base: 63_000, vol: 1.4, fund: 0.000042, trend: 0.6 }, + ETH: { base: 2_480, vol: 1.9, fund: 0.000031, trend: 0.3 }, + SOL: { base: 148, vol: 2.8, fund: -0.000026, trend: -0.4 }, +} + +function baseFor(coin: string) { + if (BASES[coin]) return BASES[coin] + const h = hashStr(coin) + return { base: 5 + (h % 400), vol: 2 + (h % 5), fund: ((h % 7) - 3) / 100000, trend: ((h % 5) - 2) / 4 } +} + +const HOUR = 3_600_000 + +export function seedCandles(coin: string): Candle[] { + const { base, vol, trend } = baseFor(coin) + const rnd = mulberry32(hashStr(coin) + 99) + const out: Candle[] = [] + let p = base * (1 - vol / 100 - 0.02) + const now = 1_700_000_000_000 // fixed epoch so seed is fully deterministic + for (let i = 0; i < 120; i++) { + const wave = Math.sin(i / 9) * vol * 0.35 + trend * 0.04 + const shock = (rnd() - 0.5) * vol + p = Math.max(p * (1 + (wave + shock) / 100), base * 0.7) + const o = p * (1 - (rnd() - 0.5) / 220) + const c = p + const h = Math.max(o, c) * (1 + rnd() / 320) + const l = Math.min(o, c) * (1 - rnd() / 320) + out.push({ t: now + i * HOUR, o, h, l, c, v: 400 + rnd() * 900 }) + } + // ease final close toward base for a believable "current" price + out[out.length - 1].c = base + return out +} + +export function seedStat(coin: string): MarketStat { + const { base, fund } = baseFor(coin) + const rnd = mulberry32(hashStr(coin) + 7) + const changePct = (rnd() - 0.4) * 4 + const prev = base / (1 + changePct / 100) + const oi = base * (800 + rnd() * 4000) + return { + coin, + mark: base, + mid: base, + prevDayPx: prev, + changePct, + funding: fund, + openInterest: oi / base, + oiUsd: oi, + dayVolumeUsd: base * (5000 + rnd() * 40000) * 100, + } +} + +export const clampUnit = (n: number) => clamp(n, 0, 1) diff --git a/src/hooks/useHyperliquid.ts b/src/hooks/useHyperliquid.ts new file mode 100644 index 0000000..55fc4cc --- /dev/null +++ b/src/hooks/useHyperliquid.ts @@ -0,0 +1,135 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { getCandles, getMarketStats, type Candle, type MarketStat } from '@/lib/hyperliquid' +import { summarizeTA, type TASnapshot } from '@/lib/ta' +import { screen as screenTA, type Vote, type Verdict } from '@/lib/signals' +import { seedCandles, seedStat } from '@/data/seedMarket' + +type Async = { + data: T | null + loading: boolean + live: boolean + error: string | null + updatedAt: number +} + +/** Live market stats for a set of perps; polls politely and keeps last-good on error. */ +export function useMarketStats(coins: string[], pollMs = 10_000): Async { + const key = coins.join(',') + const [state, setState] = useState>(() => ({ + data: coins.map(seedStat), + loading: false, + live: false, + error: null, + updatedAt: 0, + })) + + useEffect(() => { + let alive = true + let timer: ReturnType + const tick = async () => { + const ctrl = new AbortController() + const t = setTimeout(() => ctrl.abort(), 12_000) + try { + const data = await getMarketStats(coins, ctrl.signal) + if (alive) + setState({ data, loading: false, live: true, error: null, updatedAt: Date.now() }) + } catch (e) { + if (alive) + setState((s) => ({ ...s, loading: false, live: false, error: (e as Error).message })) + } finally { + clearTimeout(t) + if (alive) timer = setTimeout(tick, pollMs) + } + } + tick() + return () => { + alive = false + clearTimeout(timer) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [key, pollMs]) + + return state +} + +export type CoinScreen = { + candles: Candle[] + ta: TASnapshot + stat: MarketStat | null + votes: Vote[] + verdict: Verdict +} + +/** Live candle screen for one coin: real candles → TA → agent votes → verdict. */ +export function useScreen( + coin: string, + interval = '1h', + pollMs = 15_000, +): Async & { reload: () => void } { + const seed = useMemo(() => { + const candles = seedCandles(coin) + const stat = seedStat(coin) + const ta = summarizeTA(candles, stat.funding) + const { votes, verdict } = screenTA(ta, stat) + return { candles, ta, stat, votes, verdict } + }, [coin]) + + const [state, setState] = useState>(() => ({ + data: seed, + loading: false, + live: false, + error: null, + updatedAt: 0, + })) + const lastGood = useRef(seed) + const reloadRef = useRef<() => void>(() => {}) + + useEffect(() => { + // show this coin's seed immediately, then chase live data. + // reset updatedAt so the UI knows this coin has no live data yet (→ chart shows skeleton, + // not the instant seed snapshot) until the first successful fetch for THIS coin lands. + lastGood.current = seed + setState((s) => ({ ...s, data: seed, live: false, updatedAt: 0 })) + let alive = true + let timer: ReturnType + const tick = async () => { + const ctrl = new AbortController() + const t = setTimeout(() => ctrl.abort(), 12_000) + try { + const [candles, stats] = await Promise.all([ + getCandles(coin, interval, 120, ctrl.signal), + getMarketStats([coin], ctrl.signal), + ]) + const stat = stats[0] ?? null + const ta = summarizeTA(candles, stat?.funding ?? 0) + const { votes, verdict } = screenTA(ta, stat ?? undefined) + const data: CoinScreen = { candles, ta, stat, votes, verdict } + lastGood.current = data + if (alive) + setState({ data, loading: false, live: true, error: null, updatedAt: Date.now() }) + } catch (e) { + if (alive) + setState((s) => ({ + ...s, + data: s.data ?? lastGood.current, + loading: false, + live: false, + error: (e as Error).message, + })) + } finally { + clearTimeout(t) + if (alive) timer = setTimeout(tick, pollMs) + } + } + reloadRef.current = () => { + if (alive) tick() + } + tick() + return () => { + alive = false + clearTimeout(timer) + } + }, [coin, interval, pollMs, seed]) + + return { ...state, reload: () => reloadRef.current() } +} diff --git a/src/hooks/useInView.ts b/src/hooks/useInView.ts new file mode 100644 index 0000000..e44d49d --- /dev/null +++ b/src/hooks/useInView.ts @@ -0,0 +1,70 @@ +import { useEffect, useRef, useState } from 'react' + +type Opts = { once?: boolean; threshold?: number; rootMargin?: string } + +/** + * Returns a ref + whether it has entered the viewport. + * Primary: IntersectionObserver. Fallback: a rAF-throttled rect check on + * scroll/resize — so content never stays stuck hidden if IO is throttled + * (offscreen tab, some headless/embedded webviews). + */ +export function useInView(opts: Opts = {}) { + const { once = true, threshold = 0.18, rootMargin = '0px 0px -8% 0px' } = opts + const ref = useRef(null) + const [inView, setInView] = useState(false) + + useEffect(() => { + const el = ref.current + if (!el) return + let revealed = false + let raf = 0 + + const apply = (visible: boolean) => { + if (once) { + if (visible && !revealed) { + revealed = true + setInView(true) + cleanup() + } + } else { + setInView(visible) + } + } + + const rectCheck = () => { + const r = el.getBoundingClientRect() + const vh = window.innerHeight || document.documentElement.clientHeight + apply(r.top < vh * 0.9 && r.bottom > vh * 0.04) + } + + let io: IntersectionObserver | null = null + if (typeof IntersectionObserver !== 'undefined') { + io = new IntersectionObserver( + (entries) => { + for (const e of entries) apply(e.isIntersecting) + }, + { threshold, rootMargin }, + ) + io.observe(el) + } + + const onScroll = () => { + cancelAnimationFrame(raf) + raf = requestAnimationFrame(rectCheck) + } + + rectCheck() + window.addEventListener('scroll', onScroll, { passive: true }) + window.addEventListener('resize', onScroll) + + function cleanup() { + cancelAnimationFrame(raf) + io?.disconnect() + window.removeEventListener('scroll', onScroll) + window.removeEventListener('resize', onScroll) + } + return cleanup + }, [once, threshold, rootMargin]) + + return [ref, inView] as const +} diff --git a/src/hooks/useMarketBrief.ts b/src/hooks/useMarketBrief.ts new file mode 100644 index 0000000..ae3d007 --- /dev/null +++ b/src/hooks/useMarketBrief.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react' +import { getBrief, FACTSAI_ENABLED, type Brief } from '@/lib/factsai' + +type State = { data: Brief | null; loading: boolean; live: boolean; error: string | null } + +/** Fetches the research-agent brief once (cached). live=false → caller may hide the section. */ +export function useMarketBrief(query: string): State { + const [state, setState] = useState({ + data: null, + loading: FACTSAI_ENABLED, + live: false, + error: FACTSAI_ENABLED ? null : 'disabled', + }) + + useEffect(() => { + if (!FACTSAI_ENABLED) return + let alive = true + const ctrl = new AbortController() + getBrief(query, ctrl.signal) + .then((b) => alive && setState({ data: b, loading: false, live: true, error: null })) + .catch((e) => alive && setState({ data: null, loading: false, live: false, error: String(e?.message ?? e) })) + return () => { + alive = false + ctrl.abort() + } + }, [query]) + + return state +} diff --git a/src/hooks/usePrefersReducedMotion.ts b/src/hooks/usePrefersReducedMotion.ts new file mode 100644 index 0000000..0129c6d --- /dev/null +++ b/src/hooks/usePrefersReducedMotion.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' + +/** Tracks the user's reduced-motion preference, reactively. */ +export function usePrefersReducedMotion(): boolean { + const [reduced, setReduced] = useState( + () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches, + ) + useEffect(() => { + const mq = window.matchMedia('(prefers-reduced-motion: reduce)') + const on = () => setReduced(mq.matches) + mq.addEventListener('change', on) + return () => mq.removeEventListener('change', on) + }, []) + return reduced +} diff --git a/src/hooks/useScrollProgress.ts b/src/hooks/useScrollProgress.ts new file mode 100644 index 0000000..cbaaf3d --- /dev/null +++ b/src/hooks/useScrollProgress.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react' + +/** Whole-page scroll progress 0..1 (for the nav progress hairline). */ +export function useScrollProgress(): number { + const [p, setP] = useState(0) + useEffect(() => { + let raf = 0 + const on = () => { + cancelAnimationFrame(raf) + raf = requestAnimationFrame(() => { + const h = document.documentElement.scrollHeight - window.innerHeight + setP(h > 0 ? Math.min(1, Math.max(0, window.scrollY / h)) : 0) + }) + } + on() + window.addEventListener('scroll', on, { passive: true }) + window.addEventListener('resize', on) + return () => { + window.removeEventListener('scroll', on) + window.removeEventListener('resize', on) + cancelAnimationFrame(raf) + } + }, []) + return p +} diff --git a/src/lib/factsai.ts b/src/lib/factsai.ts new file mode 100644 index 0000000..477a948 --- /dev/null +++ b/src/lib/factsai.ts @@ -0,0 +1,85 @@ +/** + * FactsAI deep-research API — https://factsai.org/docs + * POST /answer { query } -> { success, data: { answer, citations[] } } + * + * The "research agent": real AI-generated market brief with cited web sources. + * Direct browser calls need CORS + an exposed key; prefer a proxy in production + * (set VITE_FACTSAI_PROXY). Successful answers are cached in localStorage (TTL) to + * keep cost down (each call is billed). + */ +const env = import.meta.env as Record +const KEY = env.VITE_FACTSAI_KEY +const PROXY = env.VITE_FACTSAI_PROXY +const BASE = PROXY || 'https://deep-research-api.degodmode3-33.workers.dev' +const ENDPOINT = `${BASE.replace(/\/$/, '')}/answer` + +const CACHE_KEY = 'quant.brief.v1' +const TTL = 30 * 60 * 1000 // 30 min + +export type Citation = { + id: string + url: string + title: string + author?: string + publishedDate?: string + favicon?: string +} + +export type Brief = { answer: string; citations: Citation[]; at: number } + +export const FACTSAI_ENABLED = Boolean(KEY || PROXY) + +function readCache(query: string): Brief | null { + try { + const raw = localStorage.getItem(CACHE_KEY) + if (!raw) return null + const c = JSON.parse(raw) as Brief & { q?: string } + if (c.q === query && Date.now() - c.at < TTL) return c + } catch { + /* ignore */ + } + return null +} + +export async function getBrief(query: string, signal?: AbortSignal): Promise { + const cached = readCache(query) + if (cached) return cached + if (!FACTSAI_ENABLED) throw new Error('FactsAI not configured') + + const res = await fetch(ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // when using a proxy, the proxy injects the key; only send it on direct calls + ...(KEY && !PROXY ? { Authorization: `Bearer ${KEY}` } : {}), + }, + body: JSON.stringify({ query }), + signal, + }) + if (!res.ok) throw new Error(`FactsAI ${res.status}`) + const json = (await res.json()) as { + success?: boolean + error?: string + data?: { answer: string; citations?: Citation[] } + } + if (!json.success || !json.data) throw new Error(json.error || 'FactsAI error') + + const brief: Brief = { + answer: json.data.answer, + citations: (json.data.citations || []).slice(0, 6).map((c) => ({ + id: c.id, + url: c.url, + title: c.title, + author: c.author, + publishedDate: c.publishedDate, + favicon: c.favicon, + })), + at: Date.now(), + } + try { + localStorage.setItem(CACHE_KEY, JSON.stringify({ ...brief, q: query })) + } catch { + /* ignore */ + } + return brief +} diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..48122b7 --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,45 @@ +/** Locale-aware formatters. Tabular figures recommended (see .tnum utility). */ + +export function fmtUsd(n: number, maxFrac = 2): string { + if (!isFinite(n)) return '—' + return n.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: maxFrac, + }) +} + +export function fmtPrice(n: number): string { + if (!isFinite(n)) return '—' + const frac = n >= 1000 ? 0 : n >= 1 ? 2 : 4 + return n.toLocaleString('en-US', { + minimumFractionDigits: frac, + maximumFractionDigits: frac, + }) +} + +/** 1_234_567 → "1.23M" */ +export function fmtCompact(n: number, digits = 2): string { + if (!isFinite(n)) return '—' + const abs = Math.abs(n) + const sign = n < 0 ? '-' : '' + if (abs >= 1e9) return `${sign}${(abs / 1e9).toFixed(digits)}B` + if (abs >= 1e6) return `${sign}${(abs / 1e6).toFixed(digits)}M` + if (abs >= 1e3) return `${sign}${(abs / 1e3).toFixed(digits)}K` + return `${sign}${abs.toFixed(0)}` +} + +export function fmtPct(n: number, digits = 2, signed = true): string { + if (!isFinite(n)) return '—' + const s = signed && n > 0 ? '+' : '' + return `${s}${n.toFixed(digits)}%` +} + +export function fmtNum(n: number, digits = 0): string { + if (!isFinite(n)) return '—' + return n.toLocaleString('en-US', { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }) +} diff --git a/src/lib/hyperliquid.ts b/src/lib/hyperliquid.ts new file mode 100644 index 0000000..6a2b2d8 --- /dev/null +++ b/src/lib/hyperliquid.ts @@ -0,0 +1,112 @@ +/** + * Hyperliquid public market data (no auth, CORS-enabled — verified). + * Docs: https://hyperliquid.gitbook.io · POST https://api.hyperliquid.xyz/info + */ + +const INFO_URL = 'https://api.hyperliquid.xyz/info' + +export type Candle = { t: number; o: number; h: number; l: number; c: number; v: number } + +export type MarketStat = { + coin: string + mark: number + mid: number + prevDayPx: number + changePct: number + funding: number // hourly funding rate + openInterest: number // in base asset + oiUsd: number + dayVolumeUsd: number +} + +type RawCandle = { t: number; T: number; o: string; c: string; h: string; l: string; v: string; n: number } +type Universe = { universe: { name: string; szDecimals: number; maxLeverage: number; isDelisted?: boolean }[] } +type AssetCtx = { + funding: string + openInterest: string + prevDayPx: string + dayNtlVlm: string + markPx: string + midPx: string | null + oraclePx: string +} + +async function info(body: unknown, signal?: AbortSignal): Promise { + const res = await fetch(INFO_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + signal, + }) + if (!res.ok) throw new Error(`Hyperliquid ${res.status}`) + return res.json() as Promise +} + +export async function getAllMids(signal?: AbortSignal): Promise> { + return info>({ type: 'allMids' }, signal) +} + +/** Live mark/mid/funding/OI/volume for the requested coins (perp names like "BTC"). */ +export async function getMarketStats(coins: string[], signal?: AbortSignal): Promise { + const [meta, ctxs] = await info<[Universe, AssetCtx[]]>({ type: 'metaAndAssetCtxs' }, signal) + const out: MarketStat[] = [] + for (const coin of coins) { + const idx = meta.universe.findIndex((u) => u.name === coin) + if (idx < 0) continue + const c = ctxs[idx] + if (!c) continue + const mark = +c.markPx + const mid = c.midPx ? +c.midPx : mark + const prev = +c.prevDayPx + const oi = +c.openInterest + out.push({ + coin, + mark, + mid, + prevDayPx: prev, + changePct: prev ? ((mark - prev) / prev) * 100 : 0, + funding: +c.funding, + openInterest: oi, + oiUsd: oi * mark, + dayVolumeUsd: +c.dayNtlVlm, + }) + } + return out +} + +const INTERVAL_MS: Record = { + '1m': 60_000, + '5m': 300_000, + '15m': 900_000, + '1h': 3_600_000, + '4h': 14_400_000, + '1d': 86_400_000, +} + +/** Most recent `lookback` candles for a coin/interval. */ +export async function getCandles( + coin: string, + interval = '1h', + lookback = 120, + signal?: AbortSignal, +): Promise { + const step = INTERVAL_MS[interval] ?? INTERVAL_MS['1h'] + const endTime = Date.now() + const startTime = endTime - step * (lookback + 2) + const raw = await info( + { type: 'candleSnapshot', req: { coin, interval, startTime, endTime } }, + signal, + ) + return raw.map((r) => ({ + t: r.t, + o: +r.o, + h: +r.h, + l: +r.l, + c: +r.c, + v: +r.v, + })) +} + +/** Default tradeable universe for the site. */ +export const SYMBOLS = ['BTC', 'ETH', 'SOL'] as const +export type Symbol = (typeof SYMBOLS)[number] diff --git a/src/lib/seed.ts b/src/lib/seed.ts new file mode 100644 index 0000000..322d0e5 --- /dev/null +++ b/src/lib/seed.ts @@ -0,0 +1,26 @@ +/** Deterministic helpers — so "live" agent decisions are stable per render, not random flicker. */ + +/** Seeded PRNG (mulberry32). Same seed → same stream. */ +export function mulberry32(seed: number) { + let a = seed >>> 0 + return function () { + a |= 0 + a = (a + 0x6d2b79f5) | 0 + let t = Math.imul(a ^ (a >>> 15), 1 | a) + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} + +/** FNV-1a string hash → uint32 seed. */ +export function hashStr(s: string): number { + let h = 2166136261 + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i) + h = Math.imul(h, 16777619) + } + return h >>> 0 +} + +export const clamp = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n)) +export const lerp = (a: number, b: number, t: number) => a + (b - a) * t diff --git a/src/lib/signals.ts b/src/lib/signals.ts new file mode 100644 index 0000000..e6d57a1 --- /dev/null +++ b/src/lib/signals.ts @@ -0,0 +1,151 @@ +/** + * Signal layer. Each agent reads the (real) TA snapshot and votes. The aggregate + * produces the final LONG / SHORT / SKIP verdict with confidence and risk params. + * Deterministic: same market state → same decision (no random flicker). + */ +import type { TASnapshot } from './ta' +import type { MarketStat } from './hyperliquid' +import { clamp } from './seed' + +export type Direction = 'LONG' | 'SHORT' | 'SKIP' + +export type Vote = { + agent: string + layer: number + direction: Direction + score: number // 0..1 conviction + reason: string +} + +export type Verdict = { + decision: Direction + confidence: number // 0..1 + sizePct: number + tpPct: number + slPct: number + reasoning: string + net: number +} + +const round = (n: number, d = 2) => Math.round(n * 10 ** d) / 10 ** d + +export function agentVotes(ta: TASnapshot, mkt?: MarketStat): Vote[] { + const votes: Vote[] = [] + + // ── TrendAgent: EMA9/21 crossover + RSI bias (1h) + { + const long = ta.ema9 > ta.ema21 && ta.rsi > 50 + const short = ta.ema9 < ta.ema21 && ta.rsi < 50 + const dir: Direction = long ? 'LONG' : short ? 'SHORT' : 'SKIP' + const score = clamp(Math.abs(ta.emaDiffPct) * 14 + Math.abs(ta.rsi - 50) / 60, 0, 1) + votes.push({ + agent: 'TrendAgent', + layer: 3, + direction: dir, + score: round(dir === 'SKIP' ? score * 0.4 : score), + reason: + dir === 'SKIP' + ? `EMA9/21 flat, RSI ${ta.rsi.toFixed(0)} — no trend` + : `EMA9 ${ta.ema9 > ta.ema21 ? 'above' : 'below'} EMA21, RSI ${ta.rsi.toFixed(0)}`, + }) + } + + // ── BreakoutAgent: Bollinger band push + { + const dir: Direction = ta.pctB > 0.92 ? 'LONG' : ta.pctB < 0.08 ? 'SHORT' : 'SKIP' + const score = clamp(Math.abs(ta.pctB - 0.5) * 1.7, 0, 1) + votes.push({ + agent: 'BreakoutAgent', + layer: 3, + direction: dir, + score: round(dir === 'SKIP' ? score * 0.3 : score), + reason: + dir === 'SKIP' + ? `Price mid-band (%B ${ta.pctB.toFixed(2)})` + : `%B ${ta.pctB.toFixed(2)} — ${dir === 'LONG' ? 'upside' : 'downside'} breakout`, + }) + } + + // ── MeanReversionAgent: RSI extremes + band + { + const long = ta.rsi < 32 && ta.pctB < 0.15 + const short = ta.rsi > 68 && ta.pctB > 0.85 + const dir: Direction = long ? 'LONG' : short ? 'SHORT' : 'SKIP' + const score = long + ? clamp((32 - ta.rsi) / 22, 0, 1) + : short + ? clamp((ta.rsi - 68) / 22, 0, 1) + : 0 + votes.push({ + agent: 'MeanReversionAgent', + layer: 3, + direction: dir, + score: round(score), + reason: + dir === 'SKIP' + ? `RSI ${ta.rsi.toFixed(0)} not extreme` + : `RSI ${ta.rsi.toFixed(0)} ${dir === 'LONG' ? 'oversold' : 'overbought'}, fade`, + }) + } + + // ── FundingAgent: crowded funding → fade + { + const dir: Direction = ta.fundingZ > 1.5 ? 'SHORT' : ta.fundingZ < -1.5 ? 'LONG' : 'SKIP' + const score = clamp(Math.abs(ta.fundingZ) / 3, 0, 1) + const fr = mkt ? (mkt.funding * 100).toFixed(4) : '—' + votes.push({ + agent: 'FundingAgent', + layer: 3, + direction: dir, + score: round(dir === 'SKIP' ? score * 0.4 : score), + reason: + dir === 'SKIP' + ? `Funding neutral (z ${ta.fundingZ.toFixed(1)})` + : `Funding ${fr}% (z ${ta.fundingZ.toFixed(1)}) — ${dir === 'SHORT' ? 'longs crowded' : 'shorts crowded'}`, + }) + } + + return votes +} + +export function aggregate(votes: Vote[], ta: TASnapshot): Verdict { + let net = 0 + let weight = 0 + for (const v of votes) { + if (v.direction === 'SKIP') continue + net += (v.direction === 'LONG' ? 1 : -1) * v.score + weight += v.score + } + const active = votes.filter((v) => v.direction !== 'SKIP') + const agreeing = active.filter((v) => Math.sign(net) === (v.direction === 'LONG' ? 1 : -1)) + const agreement = active.length ? agreeing.length / active.length : 0 + + const thr = 0.5 + const decision: Direction = net > thr ? 'LONG' : net < -thr ? 'SHORT' : 'SKIP' + + const confidence = + decision === 'SKIP' + ? clamp(0.3 + Math.abs(net) * 0.2, 0.18, 0.5) + : clamp(0.45 + Math.abs(net) * 0.18 + agreement * 0.18, 0, 0.97) + + // risk params scale with volatility (ATR%) + const vol = clamp(ta.atrPct, 0.4, 6) + const tpPct = round(clamp(vol * 1.6, 0.8, 6), 1) + const slPct = round(clamp(vol * 0.9, 0.6, 3.5), 1) + const sizePct = round(clamp(confidence * 3.2, 0.5, 3.2), 1) + + const lead = active.sort((a, b) => b.score - a.score)[0] + const reasoning = + decision === 'SKIP' + ? `Signals split (net ${net.toFixed(2)}). RiskManager holds — no edge above threshold.` + : `${agreeing.length}/${active.length} agents align ${decision}. ${lead?.reason ?? ''}. RR ${(tpPct / slPct).toFixed(1)}.` + + return { decision, confidence: round(confidence), sizePct, tpPct, slPct, reasoning, net: round(net) } +} + +/** Convenience: full screen from a TA snapshot. */ +export function screen(ta: TASnapshot, mkt?: MarketStat) { + const votes = agentVotes(ta, mkt) + const verdict = aggregate(votes, ta) + return { votes, verdict } +} diff --git a/src/lib/ta.ts b/src/lib/ta.ts new file mode 100644 index 0000000..df9f480 --- /dev/null +++ b/src/lib/ta.ts @@ -0,0 +1,132 @@ +/** + * Technical-analysis engine. These are *genuinely correct* indicators computed + * from real Hyperliquid candles — the credible backbone under the agent theatre. + */ +import type { Candle } from './hyperliquid' +import { clamp } from './seed' + +const avg = (a: number[]) => a.reduce((s, x) => s + x, 0) / (a.length || 1) + +function stdev(a: number[], mean = avg(a)): number { + if (a.length < 2) return 0 + return Math.sqrt(a.reduce((s, x) => s + (x - mean) ** 2, 0) / a.length) +} + +export function ema(values: number[], period: number): number[] { + const k = 2 / (period + 1) + const out: number[] = [] + let prev = values[0] ?? 0 + for (let i = 0; i < values.length; i++) { + prev = i === 0 ? values[0] : values[i] * k + prev * (1 - k) + out.push(prev) + } + return out +} + +/** Wilder's RSI. */ +export function rsi(values: number[], period = 14): number[] { + const out: number[] = new Array(values.length).fill(NaN) + if (values.length < period + 1) return out + let gain = 0 + let loss = 0 + for (let i = 1; i <= period; i++) { + const d = values[i] - values[i - 1] + if (d >= 0) gain += d + else loss -= d + } + let avgGain = gain / period + let avgLoss = loss / period + out[period] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss) + for (let i = period + 1; i < values.length; i++) { + const d = values[i] - values[i - 1] + const g = d > 0 ? d : 0 + const l = d < 0 ? -d : 0 + avgGain = (avgGain * (period - 1) + g) / period + avgLoss = (avgLoss * (period - 1) + l) / period + out[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss) + } + return out +} + +export function macd(values: number[], fast = 12, slow = 26, signalP = 9) { + const emaFast = ema(values, fast) + const emaSlow = ema(values, slow) + const macdLine = values.map((_, i) => emaFast[i] - emaSlow[i]) + const signalLine = ema(macdLine, signalP) + const hist = macdLine.map((v, i) => v - signalLine[i]) + return { macdLine, signalLine, hist } +} + +export function atr(candles: Candle[], period = 14): number { + if (candles.length < 2) return 0 + const tr: number[] = [] + for (let i = 0; i < candles.length; i++) { + const c = candles[i] + if (i === 0) tr.push(c.h - c.l) + else { + const pc = candles[i - 1].c + tr.push(Math.max(c.h - c.l, Math.abs(c.h - pc), Math.abs(c.l - pc))) + } + } + return ema(tr, period).at(-1) ?? 0 +} + +/** Bollinger %B — position of last price inside the band (0 = lower, 1 = upper). */ +export function bollingerPctB(values: number[], period = 20, mult = 2): number { + if (values.length < period) return 0.5 + const slice = values.slice(-period) + const mean = avg(slice) + const sd = stdev(slice, mean) + const upper = mean + mult * sd + const lower = mean - mult * sd + const last = values.at(-1) as number + const span = upper - lower + return span ? clamp((last - lower) / span, -0.2, 1.2) : 0.5 +} + +export type TASnapshot = { + price: number + rsi: number + ema9: number + ema21: number + ema50: number + emaDiffPct: number + macdHist: number + atr: number + atrPct: number + pctB: number + changePct: number + fundingZ: number + closes: number[] +} + +/** Summarize a candle series into the latest indicator readings. */ +export function summarizeTA(candles: Candle[], funding = 0): TASnapshot { + const closes = candles.map((c) => c.c) + const price = closes.at(-1) ?? 0 + const e9 = ema(closes, 9).at(-1) ?? price + const e21 = ema(closes, 21).at(-1) ?? price + const e50 = ema(closes, 50).at(-1) ?? price + const rsiArr = rsi(closes) + const rsiVal = rsiArr.at(-1) + const { hist } = macd(closes) + const a = atr(candles) + const first = closes[0] ?? price + // funding hourly rate → pseudo z-score against a typical band (illustrative) + const fundingZ = clamp(funding / 0.00005, -4, 4) + return { + price, + rsi: isFinite(rsiVal as number) ? (rsiVal as number) : 50, + ema9: e9, + ema21: e21, + ema50: e50, + emaDiffPct: price ? ((e9 - e21) / price) * 100 : 0, + macdHist: hist.at(-1) ?? 0, + atr: a, + atrPct: price ? (a / price) * 100 : 0, + pctB: bollingerPctB(closes), + changePct: first ? ((price - first) / first) * 100 : 0, + fundingZ, + closes, + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..3b43e84 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,7 @@ +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +/** Merge conditional + Tailwind classes with conflict resolution. */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..97188bc --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './styles/globals.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/pages/Landing.tsx b/src/pages/Landing.tsx new file mode 100644 index 0000000..bd90a1e --- /dev/null +++ b/src/pages/Landing.tsx @@ -0,0 +1,210 @@ +import { Link } from 'react-router-dom' +import { LogoMark } from '@/components/brand/Logo' +import { GlobeCanvas } from '@/components/landing/GlobeCanvas' +import { AgentFlow } from '@/components/landing/AgentFlow' + +const REAL = [ + { k: 'L1', t: 'Live Hyperliquid data', d: 'Real mark price, funding, open interest, volume and OHLC candles — straight from the public API.' }, + { k: 'L2', t: 'In-browser technical analysis', d: 'RSI · MACD · EMA · ATR · Bollinger · funding-Z, recomputed every poll. No black box.' }, + { k: 'L3', t: 'A committee of 4 agents', d: 'Trend, Breakout, Mean-Rev and Funding each vote LONG / SHORT / SKIP with a reason.' }, + { k: 'L4', t: 'One transparent verdict', d: 'Votes aggregate into a deterministic call with confidence, size, TP and SL. Same market → same decision.' }, +] + +function CTA() { + return ( +
+ + Open terminal + + + + How it works ↓ + +
+ ) +} + +export function Landing() { + return ( +
+ {/* ── top nav ── */} +
+
+ + + Quant Agent + + + + Terminal ↗ + +
+
+ + {/* ── hero ── */} +
+ + {/* left scrim so the headline stays legible over the globe */} +
+
+ +
+
+
+ + Multi-agent · Hyperliquid perps · paper · dry-run +
+

+ Four agents argue. +
+ + One verdict. + +

+

+ A real-time quant terminal for Hyperliquid perps. Live market data → in-browser + technical analysis → a committee of strategy agents → LONG / SHORT / SKIP{' '} + with confidence and risk parameters. Paper-traded, fully transparent. +

+
+ +
+
+
+
+ + {/* ── how agents work ── */} +
+
+ How it works +

+ Data in. The call out. +

+

+ Every cycle runs the same five steps: pull the data, compute the analysis, let four + agents vote, resolve one verdict, push it to Telegram. It plays below on a loop — + tap any step to jump to it. +

+
+ +
+ +
+ +
+

+ Looping demo. Execution is paper (dry-run) — no orders are signed. +

+ + See it live on the terminal → + +
+
+ + {/* ── what's real ── */} +
+ What's real +
+ {REAL.map((r) => ( +
+
{r.k}
+
{r.t}
+

{r.d}

+
+ ))} +
+
+ + {/* ── footer (strict Carbon Desk terminal style) ── */} +
+
+
+
+ + + Quant Agent + Terminal + +

+ A transparent multi-agent quant terminal on Hyperliquid. Live data → verdict → Telegram. +

+
+ + + LIVE · HL + + + PAPER · DRY-RUN + +
+
+ + + + +
+ +
+

+ Market data is live from the Hyperliquid public API; indicators, agent votes and the + verdict are computed in-browser. Execution is paper (dry-run).{' '} + Not financial advice. +

+ © Quant Agent · pre-TGE +
+
+
+
+ ) +} + +function FooterCol({ + title, + links, +}: { + title: string + links: { label: string; to?: string; href?: string }[] +}) { + return ( +
+ ) +} diff --git a/src/pages/Terminal.tsx b/src/pages/Terminal.tsx new file mode 100644 index 0000000..4cfb1e4 --- /dev/null +++ b/src/pages/Terminal.tsx @@ -0,0 +1,558 @@ +import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import { LogoMark } from '@/components/brand/Logo' +import { TVChart, ChartSkeleton } from '@/components/viz/TVChart' +import { LiveFeed } from '@/components/viz/LiveFeed' +import { Orchestra } from '@/components/viz/Orchestra' +import { AgentWorkflow } from '@/components/viz/AgentWorkflow' +import { Onboarding, TOUR_EVENT } from '@/components/Onboarding' +import { useScreen } from '@/hooks/useHyperliquid' +import { fmtPrice, fmtPct, fmtCompact } from '@/lib/format' +import type { Direction, Vote } from '@/lib/signals' + +const COINS = ['BTC', 'ETH', 'SOL'] +const TIMEFRAMES = ['1m', '5m', '15m', '1h', '4h', '1d'] as const + +const AGENT_META: Record = { + TrendAgent: { label: 'Trend', reads: 'EMA 9/21 · RSI' }, + BreakoutAgent: { label: 'Breakout', reads: 'Bollinger %B' }, + MeanReversionAgent: { label: 'Mean-Rev', reads: 'RSI extremes' }, + FundingAgent: { label: 'Funding', reads: 'funding-Z' }, +} +const AGENT_RULES: Record = { + TrendAgent: 'Trend — LONG when EMA9 > EMA21 and RSI > 50; SHORT on the reverse; else SKIP.', + BreakoutAgent: 'Breakout — LONG when price pushes the upper Bollinger band (%B > 0.92); SHORT at the lower (< 0.08).', + MeanReversionAgent: 'Mean-Rev — fade extremes: LONG when RSI < 32 in the lower band; SHORT when RSI > 68 at the upper.', + FundingAgent: 'Funding — fade crowded funding: SHORT when funding-Z > 1.5; LONG when < −1.5.', +} +const METRIC_INFO: Record = { + Mark: 'Mark price — the fair price Hyperliquid uses for PnL & liquidation, not the last trade.', + '24h Δ': 'Price change over the last 24 hours.', + Funding: 'Funding rate — periodic longs↔shorts payment. Positive = longs pay (crowded longs).', + ATR: 'Average True Range — volatility; it scales the verdict TP / SL.', + 'Open interest': 'Open interest — total USD value of open perpetual contracts.', + '24h volume': '24-hour traded volume, in USD.', + 'RSI (14)': 'RSI — momentum 0–100; >70 overbought, <30 oversold. Feeds Mean-Rev.', + 'Bollinger %B': 'Where price sits in the Bollinger band (0 = lower, 1 = upper). Feeds Breakout.', + 'EMA 9/21': 'Fast vs slow moving average — trend direction. Feeds Trend.', + 'Funding-Z': 'Normalised funding skew. Feeds the Funding agent.', + 'MACD hist': 'MACD histogram — momentum (MACD line minus its signal).', + 'Net vote': 'Aggregate of the 4 agents (LONG +, SHORT −). Drives the verdict.', +} +const INK = 'var(--color-bone)' +const clampN = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n)) +const dirColor = (d: Direction) => + d === 'LONG' ? 'var(--color-long)' : d === 'SHORT' ? 'var(--color-short)' : 'var(--color-muted)' +const dirBg = (d: Direction) => + d === 'LONG' ? 'var(--color-long-dim)' : d === 'SHORT' ? 'var(--color-short-dim)' : 'var(--color-track)' +const sign = (n: number) => (n >= 0 ? '+' : '') + +type Inspect = (s: string | null) => void + +/* ── primitives ───────────────────────────────────────────────────────────── */ + +function Panel({ + title, + meta, + className = '', + children, +}: { + title: React.ReactNode + meta?: React.ReactNode + className?: string + children: React.ReactNode +}) { + return ( +
+
+ {title} + {meta != null && {meta}} +
+ {children} +
+ ) +} + +function Cell({ + label, + value, + tone, + sub, + bar, + barTone, + info, + onInspect, + loading, +}: { + label: string + value: string + tone?: string + sub?: string + bar?: number + barTone?: string + info?: string + onInspect?: Inspect + loading?: boolean +}) { + return ( +
onInspect(info) : undefined} + onMouseLeave={info && onInspect && !loading ? () => onInspect(null) : undefined} + > +
{label}
+ {loading ? ( +
+ ) : ( +
+ {value} +
+ )} + {sub && !loading &&
{sub}
} + {bar != null && !loading && ( +
+
+
+ )} +
+ ) +} + +function DivergeBar({ direction, score }: { direction: Direction; score: number }) { + const col = dirColor(direction) + return ( +
+
+ {direction === 'LONG' && ( +
+ )} + {direction === 'SHORT' && ( +
+ )} + {direction === 'SKIP' && ( +
+ )} +
+ ) +} + +/* ── command bar ──────────────────────────────────────────────────────────── */ + +function StatusBar({ live, ago, stale, onReload }: { live: boolean; ago: number | null; stale: boolean; onReload: () => void }) { + const [now, setNow] = useState(() => new Date()) + const [spin, setSpin] = useState(false) + useEffect(() => { + const id = setInterval(() => setNow(new Date()), 1000) + return () => clearInterval(id) + }, []) + const reload = () => { + onReload() + setSpin(true) + setTimeout(() => setSpin(false), 600) + } + const clock = now.toISOString().slice(11, 19) + const statusCol = stale ? 'var(--color-accent)' : live ? 'var(--color-long)' : 'var(--color-muted)' + const statusText = stale ? 'DATA STALE' : live ? 'LIVE · HL' : 'CONNECTING' + return ( +
+
+
+ + + + Quant Agent + + + Terminal +
+ +
+ + + {statusText} + + + {clock} UTC + + PAPER · DRY-RUN + +
+
+
+ ) +} + +/* ── compact verdict bar (the call — dense, not a hero) ───────────────────── */ + +function VerdictBar({ decision, confidence, sizePct, tpPct, slPct, net, votes, reasoning, live, loading }: { + decision: Direction; confidence: number; sizePct: number; tpPct: number; slPct: number; net: number; votes: Vote[]; reasoning: string; live: boolean; loading: boolean +}) { + if (loading) { + return ( +
+ Verdict +
+
+
+
+
+
+
+ ) + } + const active = votes.filter((v) => v.direction !== 'SKIP') + const agree = active.filter((v) => v.direction === decision) + const rr = slPct ? tpPct / slPct : 0 + const col = dirColor(decision) + const Sep = () => + const Field = ({ k, v, c }: { k: string; v: string; c?: string }) => ( + + {k} + {v} + + ) + return ( +
+ Verdict + {decision} + + 0 ? 'var(--color-long)' : net < 0 ? 'var(--color-short)' : INK} /> + {decision === 'SKIP' ? 'no consensus' : `${agree.length}/${active.length} agree`} + + + + + + {!live && ( + seed + )} + {reasoning} +
+ ) +} + +/* ── committee (selectable, dense) ────────────────────────────────────────── */ + +function Committee({ votes, net, activeAgent, onSelect, onHover, loading }: { + votes: Vote[]; net: number; activeAgent?: string | null; onSelect?: (a: string | null) => void; onHover?: (a: string | null) => void; loading?: boolean +}) { + if (loading) { + return ( + +
+ {[0, 1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+ + ) + } + const active = votes.filter((v) => v.direction !== 'SKIP') + const lean = active.length ? clampN(net / active.length, -1, 1) : 0 + return ( + +
+ {votes.map((v) => { + const meta = AGENT_META[v.agent] ?? { label: v.agent.replace('Agent', ''), reads: '' } + const signal = v.direction !== 'SKIP' + const col = dirColor(v.direction) + const on = activeAgent === v.agent + return ( + + ) + })} +
+
+ net lean +
+
+
+ {lean >= 0 ? ( +
+ ) : ( +
+ )} +
+
+ {net.toFixed(1)} + +
+
+ + ) +} + +/* ── help card (opened from the ? in the toolbar) ─────────────────────────── */ + +const GUIDE = [ + { t: 'The verdict', d: "The committee's single call — LONG (up), SHORT (down) or SKIP (no edge), with confidence and the suggested size / TP / SL." }, + { t: 'The committee', d: 'Four strategies each vote. The graph shows them feeding the verdict; the list shows each vote and why.' }, + { t: 'Interact', d: 'Switch timeframe & coin up top, hover any field to inspect it, click an agent to pin it, ↻ to refresh. Keys: 1/2/3 coin · r refresh · Esc clear.' }, +] +function GuideCard({ onClose }: { onClose: () => void }) { + return ( +
+
+ How to read this terminal +
+ + +
+
+
+ {GUIDE.map((g, i) => ( +
+
+ {i + 1} + {g.t} +
+

{g.d}

+
+ ))} +
+
+ ) +} + +/* ── page ─────────────────────────────────────────────────────────────────── */ + +export function Terminal() { + const [coin, setCoin] = useState('BTC') + const [tf, setTf] = useState('1h') + const [selectedAgent, setSelectedAgent] = useState(null) + const [hoverAgent, setHoverAgent] = useState(null) + const [inspect, setInspect] = useState(null) + const [guideOpen, setGuideOpen] = useState(false) + const activeAgent = hoverAgent ?? selectedAgent + const { data, live, updatedAt, error, reload } = useScreen(coin, tf, 12_000) + const ta = data?.ta + const v = data?.verdict + const stat = data?.stat + const votes = data?.votes ?? [] + const candles = data?.candles ?? [] + + const [, setTick] = useState(0) + useEffect(() => { + const id = setInterval(() => setTick((t) => t + 1), 1000) + return () => clearInterval(id) + }, []) + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + const el = e.target as HTMLElement | null + if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)) return + if (e.metaKey || e.ctrlKey || e.altKey) return + if (e.key === '1' || e.key === '2' || e.key === '3') setCoin(COINS[+e.key - 1]) + else if (e.key.toLowerCase() === 'r') reload() + else if (e.key === 'Escape') { + setSelectedAgent(null) + setGuideOpen(false) + } + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [reload]) + + const ago = updatedAt ? Math.max(0, Math.round((Date.now() - updatedAt) / 1000)) : null + const stale = Boolean(error) || (ago != null && ago > 40) + const loading = updatedAt === 0 // no live data for this instrument yet → show skeletons + + const changePct = stat?.changePct ?? 0 + const changeTone = changePct >= 0 ? 'var(--color-long)' : 'var(--color-short)' + const funding = stat?.funding ?? 0 + const fundingTone = funding >= 0 ? 'var(--color-short)' : 'var(--color-long)' + const macdHist = ta?.macdHist ?? 0 + const macdTone = macdHist >= 0 ? 'var(--color-long)' : 'var(--color-short)' + const emaBull = (ta?.ema9 ?? 0) >= (ta?.ema21 ?? 0) + const emaTone = emaBull ? 'var(--color-long)' : 'var(--color-short)' + const net = v?.net ?? 0 + const netTone = net > 0.001 ? 'var(--color-long)' : net < -0.001 ? 'var(--color-short)' : INK + const rsi = ta?.rsi ?? 50 + const rsiTone = rsi < 32 ? 'var(--color-long)' : rsi > 68 ? 'var(--color-short)' : INK + const decision = (v?.decision ?? 'SKIP') as Direction + const marketMeta = live ? 'LIVE · HL' : updatedAt > 0 ? 'STALE' : 'CONNECTING…' + + const onAgentHover = (a: string | null) => { + setHoverAgent(a) + setInspect(a ? AGENT_RULES[a] ?? null : null) + } + + return ( +
+ + +
+ {/* ── command bar — terminal grid: identity · instrument · timeframe ── */} +
+
+ {/* identity + help */} +
+

+ {coin}-PERP +

+ ${fmtPrice(ta?.price ?? 0)} + + {sign(changePct)}{changePct.toFixed(2)}% + + +
+ + {/* instrument switch — uniform grid cells */} +
+
Instrument
+
+ {COINS.map((c) => { + const on = c === coin + return ( + + ) + })} +
+
+ + {/* timeframe switch — uniform grid cells */} +
+
Timeframe
+
+ {TIMEFRAMES.map((t) => { + const on = t === tf + return ( + + ) + })} +
+
+
+
+ + {guideOpen && setGuideOpen(false)} />} + + {/* ── the call (compact) ── */} + + + {/* ── orchestra · the full agent pipeline ── */} + + + {/* ── agent flow · the pipeline as an interactive workflow graph ── */} + + + {/* ── workspace ── */} +
+ {/* LEFT — stats + pipeline */} +
+ +
+ + + + + + +
+
+ +
+ + + + + = 0 ? '▲' : '▼'} ${macdHist.toFixed(2)}`} tone={macdTone} info={METRIC_INFO['MACD hist']} onInspect={setInspect} loading={loading} /> + +
+
+
+ + {/* CENTER — chart */} + +
+ {updatedAt > 0 ? : } +
+
+ + {/* RIGHT — committee */} +
+ +
+
+ + {/* ── live feed · the agent chain streaming in real time ── */} + + + {/* ── inspector / status footer ── */} +
+ Inspect + + {inspect ?? 'Hover any field, agent or pipeline stage to inspect it.'} + + paper · dry-run · not advice +
+
+ + +
+ ) +} diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100644 index 0000000..0a00603 --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,273 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +/* ================================================================= + $QUANT — design tokens · "Carbon Desk" + Dark institutional quant terminal. Carbon surfaces, elevation by + lightness, one amber accent for "the call", green/red for direction, + monospace for every number. Depth = borders + surface shifts (no shadows). + ================================================================= */ +@theme { + /* ---- surfaces (carbon — higher elevation = lighter; wells recede to inset) ---- */ + --color-paper: #0b0c0e; /* page (bg-paper) */ + --color-panel: #111316; /* panel chrome */ + --color-panel-2: #181b20; /* raised: hover / active / dropdown */ + --color-inset: #0e0f12; /* recessed: data cells, wells, tracks */ + --color-ink: #0b0c0e; /* dark value for text-ink on light/accent fills */ + + /* ---- text hierarchy (4 levels, all AA on panel) ---- */ + --color-bone: #e8eaed; /* primary */ + --color-bone-dim: #aab1ba; /* secondary / supporting prose */ + --color-muted: #828a94; /* labels (4.5:1+ on panel & panel-2) */ + --color-faint: #727983; /* captions / ticks (4.5:1 on panel) */ + + /* ---- accent (single) — amber: the call / active / brand ---- */ + --color-accent: #ffb020; + --color-accent-2: #ffc961; + --color-accent-dim: rgba(255, 176, 32, 0.12); + + /* ---- trading status (semantic; the only other chroma) ---- */ + --color-long: #2bd974; + --color-long-dim: rgba(43, 217, 116, 0.14); + --color-short: #f2444d; + --color-short-dim: rgba(242, 68, 77, 0.14); + --color-lime: #2bd974; /* alias kept for legacy ui/* primitives */ + + /* ---- lines / borders + recessed track ---- */ + --color-line: rgba(255, 255, 255, 0.1); /* standard */ + --color-line-soft: rgba(255, 255, 255, 0.06); /* soft separation */ + --color-line-strong: rgba(255, 255, 255, 0.16); /* emphasis / zero-axis */ + --color-line-amber: rgba(255, 176, 32, 0.45); + --color-track: rgba(255, 255, 255, 0.09); /* unfilled bar/ring channel */ + + /* ---- type ---- */ + --font-display: 'Chakra Petch', 'Geist', ui-sans-serif, system-ui, sans-serif; + --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Geist Mono', ui-monospace, 'SF Mono', 'Roboto Mono', monospace; + + /* type scale (a ratio, not eyeballed) */ + --text-micro: 0.625rem; /* 10px — captions, codes, ticks */ + --text-label: 0.6875rem; /* 11px — field labels */ + --text-data: 0.8125rem; /* 13px — data values */ + --text-read: 0.75rem; /* 12px — reasons / prose */ + + /* ---- radii ---- */ + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 10px; +} + +@keyframes shimmer { 100% { transform: translateX(100%); } } +@keyframes fade-in { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes accordion-down { + from { height: 0; opacity: 0; } + to { height: var(--radix-accordion-content-height); opacity: 1; } +} +@keyframes accordion-up { + from { height: var(--radix-accordion-content-height); opacity: 1; } + to { height: 0; opacity: 0; } +} +@keyframes dash { to { stroke-dashoffset: 0; } } +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.15; } +} +@keyframes rise { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: none; } +} +@keyframes pulse-glow { + 0%, 100% { opacity: 0.55; } + 50% { opacity: 1; } +} +/* live-feed row reveal — flash + slide, opacity stays 1 (safe under reduced-motion) */ +@keyframes feed-in { + 0% { transform: translateX(-8px); background-color: var(--color-accent-dim); } + 100% { transform: translateX(0); background-color: transparent; } +} + +/* ================================================================= + base + ================================================================= */ +:root { color-scheme: dark; --chart-1: oklch(0.32 0 none); --chart-2: oklch(0.41 0 none); --chart-3: oklch(0.54 0 none); --chart-4: oklch(0.71 0 none); --chart-5: oklch(0.89 0 none); --chart-background: oklch(1 0 0); --chart-foreground: oklch(0.145 0.004 285); --chart-foreground-muted: oklch(0.55 0.014 260); --chart-line-primary: var(--chart-1); --chart-line-secondary: var(--chart-2); --chart-crosshair: oklch(0.4 0.1828 274.34); --chart-grid: oklch(0.9 0 0); --chart-brush-border: var(--chart-grid); --chart-tooltip-background: oklch(0.21 0.006 285 / 0.8); --chart-tooltip-foreground: oklch(0.985 0 0); --chart-tooltip-muted: oklch(0.65 0.01 260); --chart-marker-background: oklch(0.97 0.005 260); --chart-marker-border: oklch(0.85 0.01 260); --chart-marker-foreground: oklch(0.3 0.01 260); --chart-ring-background: oklch(0.9 0.005 260 / 0.25); --chart-label: oklch(0.45 0.01 260); } + +/* in @layer base so min-w-* / border-* utilities still win over this reset */ +@layer base { + * { + border-color: var(--color-line-soft); + min-width: 0; + } +} + +html { + background: var(--color-paper); + scrollbar-color: #2a2e34 #0e0f12; + scrollbar-width: thin; + -webkit-tap-highlight-color: transparent; +} + +body { + margin: 0; + background: var(--color-paper); + color: var(--color-bone); + font-family: var(--font-sans); + font-synthesis-weight: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + overflow-x: clip; +} + +/* tight technical display — Geist, weighty but not expanded */ +h1, h2, h3, h4 { + font-family: var(--font-display); + font-weight: 650; + letter-spacing: -0.02em; + line-height: 1.02; + text-wrap: balance; +} + +::selection { + background: rgba(255, 176, 32, 0.28); + color: #fff; +} + +/* guaranteed amber focus indicator for every interactive element */ +:where(a, button, input, select, textarea, [tabindex]):focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +/* ---- thin technical scrollbar ---- */ +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: #0e0f12; } +::-webkit-scrollbar-thumb { + background: #2a2e34; + border: 3px solid #0e0f12; + border-radius: 6px; +} +::-webkit-scrollbar-thumb:hover { background: #3a3f47; } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } + .blink { animation: none !important; opacity: 1 !important; } +} + +/* ================================================================= + utilities + ================================================================= */ +@layer utilities { + /* tabular figures for any numeric data — prevents column jitter */ + .tnum { + font-variant-numeric: tabular-nums; + font-feature-settings: 'tnum'; + } + + /* mono uppercase label — the single label primitive (+ -sm micro variant) */ + .label { + font-family: var(--font-mono); + font-size: var(--text-label); + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--color-muted); + } + .label-sm { + font-family: var(--font-mono); + font-size: var(--text-micro); + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-muted); + } + + .rule { border-top: 1px solid var(--color-line-strong); } + .rule-soft { border-top: 1px solid var(--color-line-soft); } + + /* blinking caret/dot for "live" */ + .blink { animation: blink 1.4s steps(2, end) infinite; } + + /* live-feed new-row reveal (flash + slide) */ + .feed-in { animation: feed-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; } + + /* skeleton shimmer for not-yet-loaded data */ + .skeleton { + position: relative; + overflow: hidden; + background: var(--color-track); + border-radius: 3px; + } + .skeleton::after { + content: ''; + position: absolute; + inset: 0; + transform: translateX(-100%); + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.07), transparent); + animation: shimmer 1.5s ease-in-out infinite; + } + + /* ── agent workflow graph — smooth edge/node state transitions ── */ + .wf-edge { transition: stroke 0.25s ease, stroke-width 0.25s ease, opacity 0.25s ease; } + .wf-node rect { transition: fill 0.2s ease, stroke 0.2s ease, stroke-width 0.2s ease; } + .wf-node { cursor: pointer; transform-box: fill-box; transform-origin: center; transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1); } + .wf-node:hover { transform: scale(1.035); } + @media (prefers-reduced-motion: reduce) { .wf-node:hover { transform: none; } } + + /* ── landing (ACHI) — bold glow + staged reveal ── */ + .reveal { opacity: 0; animation: rise 0.85s cubic-bezier(0.22, 1, 0.36, 1) both; } + .glow-amber { text-shadow: 0 0 24px rgba(255, 176, 32, 0.45), 0 0 64px rgba(255, 176, 32, 0.18); } + .glow-long { text-shadow: 0 0 24px rgba(43, 217, 116, 0.5), 0 0 64px rgba(43, 217, 116, 0.2); } + .pulse-glow { animation: pulse-glow 2.4s ease-in-out infinite; } +} + +.dark { + --chart-1: oklch(1 0 none); + --chart-2: oklch(0.73 0 none); + --chart-3: oklch(0.51 0 none); + --chart-4: oklch(0.39 0 none); + --chart-5: oklch(0.32 0 none); + --chart-background: oklch(0.145 0 0); + --chart-foreground: oklch(0.45 0 0); + --chart-foreground-muted: oklch(0.65 0.01 260); + --chart-crosshair: oklch(0.45 0 0); + --chart-grid: oklch(0.25 0 0); + --chart-brush-border: var(--chart-grid); + --chart-marker-background: oklch(0.25 0.01 260); + --chart-marker-border: oklch(0.4 0.01 260); + --chart-marker-foreground: oklch(0.9 0 0); + --chart-ring-background: oklch(0.35 0.01 260 / 0.25); + --chart-label: oklch(0.75 0.01 260); +} + +@theme inline { + --color-chart-1: var(----chart-1); + --color-chart-2: var(----chart-2); + --color-chart-3: var(----chart-3); + --color-chart-4: var(----chart-4); + --color-chart-5: var(----chart-5); + --color-chart-background: var(----chart-background); + --color-chart-foreground: var(----chart-foreground); + --color-chart-foreground-muted: var(----chart-foreground-muted); + --chart-line-primary: var(----chart-line-primary); + --chart-line-secondary: var(----chart-line-secondary); + --color-chart-crosshair: var(----chart-crosshair); + --color-chart-grid: var(----chart-grid); + --chart-brush-border: var(----chart-brush-border); + --color-chart-tooltip-background: var(----chart-tooltip-background); + --color-chart-tooltip-foreground: var(----chart-tooltip-foreground); + --color-chart-tooltip-muted: var(----chart-tooltip-muted); + --color-chart-marker-background: var(----chart-marker-background); + --color-chart-marker-border: var(----chart-marker-border); + --color-chart-marker-foreground: var(----chart-marker-foreground); + --color-chart-ring-background: var(----chart-ring-background); + --color-chart-label: var(----chart-label); +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..a86dbac --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Path alias (paths resolve relative to this file in TS 6+) */ + "paths": { + "@/*": ["./src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fec8c8e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ab33ff3 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,37 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react(), + tailwindcss(), + // svgo v4 keeps viewBox by default; defaults are fine + ViteImageOptimizer(), + ], + resolve: { + dedupe: ['react', 'react-dom'], + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + optimizeDeps: { + include: ['react', 'react-dom', 'react/jsx-runtime'], + }, + build: { + cssCodeSplit: true, + rollupOptions: { + output: { + manualChunks(id) { + if (!id.includes('node_modules')) return + if (id.includes('@visx') || id.includes('/d3-') || id.includes('motion')) return 'charts' + if (id.includes('/react') || id.includes('react-dom') || id.includes('react-router')) + return 'vendor' + }, + }, + }, + }, +}) diff --git a/worker/README.md b/worker/README.md new file mode 100644 index 0000000..87d7e6f --- /dev/null +++ b/worker/README.md @@ -0,0 +1,72 @@ +# Paper-trading worker + +The always-on runtime that makes the swarm **real** — without real-money risk. + +It reuses the exact browser brain (`src/lib/hyperliquid.ts` · `ta.ts` · `signals.ts`, +pure functions, no DOM) on a server loop, paper-trades the verdict against **live +Hyperliquid prices**, and broadcasts every fill to **Telegram**. This turns the site's +biggest fiction — the performance track record — into an honest, verifiable record. + +``` +real candles → real TA → 4 agents vote → verdict → paper open/close → Telegram + JSON + (L1) (L2) (L3) (L4) (L5 paper) (L6) +``` + +## Run + +```bash +pnpm worker # run forever, polling every QUANT_POLL_MS (default 60s) +pnpm worker:once # single tick then exit (smoke test) +``` + +No setup required — with no Telegram configured it runs head-less and prints every +signal/fill to stdout. Add a bot to broadcast (see below). + +## Telegram (optional) + +1. Create a bot with [@BotFather](https://t.me/BotFather) → copy the token. +2. Message the bot once, then get your chat id from [@userinfobot](https://t.me/userinfobot). +3. Put both in `.env.local`: + ``` + TELEGRAM_BOT_TOKEN=123456:ABC-... + TELEGRAM_CHAT_ID=123456789 + ``` +4. `pnpm worker`. The bot posts on open/close and answers commands: + `/status` · `/positions` · `/pnl` (a.k.a. `/learn`) · `/help`. + +## What's real vs. paper + +- **Real:** market data, candles, every indicator, the agent votes, the verdict, the + entry/exit *prices*, and therefore the PnL. +- **Paper:** no order is ever signed or sent. `mode` is fixed to `dry-run`. Positions are + marked to market against the live price; TP/SL are assumed filled at the level. This is + the deliberate 80/20 line — all the credibility, none of the key-management / loss risk. + +## Config (env, all optional) + +| var | default | meaning | +|---|---|---| +| `QUANT_COINS` | `BTC,ETH,SOL` | universe | +| `QUANT_INTERVAL` | `1h` | candle timeframe | +| `QUANT_POLL_MS` | `60000` | tick cadence | +| `QUANT_EQUITY_START` | `10000` | starting paper bankroll (USD) | +| `QUANT_LEVERAGE` | `5` | applied to `sizePct` → notional | +| `QUANT_CONF_FLOOR` | `0.55` | min verdict confidence to open | +| `QUANT_MAX_OPEN_PER_COIN` | `1` | concurrent positions per coin | +| `QUANT_DATA_FILE` | `worker/data/state.json` | persisted state | + +## State + +A single JSON file (`worker/data/`, gitignored) written atomically each tick: +`equity`, open `positions`, `closed` trades, `signals` log, and an `equityCurve`. It is +exactly the shape a future `/api/equity` · `/api/signals` endpoint would serve so the +site's Performance section and "recent fills" can read the real record instead of the +seeded one. + +## Next 20% (deliberately skipped) + +- **Live execution:** sign + send real Hyperliquid orders. Needs an account, key + management, and accepts real loss — keep behind a flag, `dry-run` stays default. +- **`/api/*` + frontend wiring:** serve this state and point `Performance` / fills at it. +- **Claude in the loop:** LLM-written reasoning per verdict and a real LLM `/learn` + retrospective (off the risk path). diff --git a/worker/config.ts b/worker/config.ts new file mode 100644 index 0000000..4f5c898 --- /dev/null +++ b/worker/config.ts @@ -0,0 +1,48 @@ +/** + * Worker config. Loaded from `.env.local` (gitignored) with safe defaults so the + * loop runs out-of-the-box in dry-run *paper* mode even with zero configuration. + * + * Everything here is intentionally paper-only: this process never signs or sends a + * real order. Live execution is the genuinely-risky 20% we deliberately skip — the + * `mode` is fixed to 'dry-run' so the credibility comes from an honest, verifiable + * paper track record, not from putting an LLM/agent in the money-losing path. + */ + +// Node 21+ — load .env.local into process.env without any dependency. +try { + process.loadEnvFile('.env.local') +} catch { + /* no .env.local — fall back to real env / defaults */ +} + +const num = (v: string | undefined, d: number) => + v != null && v !== '' && isFinite(+v) ? +v : d + +const list = (v: string | undefined, d: string[]) => + v ? v.split(',').map((s) => s.trim()).filter(Boolean) : d + +export const CONFIG = { + // ── universe / cadence + coins: list(process.env.QUANT_COINS, ['BTC', 'ETH', 'SOL']), + interval: process.env.QUANT_INTERVAL ?? '1h', + lookback: num(process.env.QUANT_LOOKBACK, 120), + pollMs: num(process.env.QUANT_POLL_MS, 60_000), + + // ── paper trading + equityStart: num(process.env.QUANT_EQUITY_START, 10_000), + leverage: num(process.env.QUANT_LEVERAGE, 5), + confFloor: num(process.env.QUANT_CONF_FLOOR, 0.55), + maxOpenPerCoin: num(process.env.QUANT_MAX_OPEN_PER_COIN, 1), + + // ── telegram (optional — worker logs to stdout if unset) + tgToken: process.env.TELEGRAM_BOT_TOKEN ?? '', + tgChat: process.env.TELEGRAM_CHAT_ID ?? '', + + // ── execution mode — paper only. 'live' is intentionally not implemented. + mode: 'dry-run' as const, + + // ── persistence + dataFile: process.env.QUANT_DATA_FILE ?? 'worker/data/state.json', +} + +export type Config = typeof CONFIG diff --git a/worker/loop.ts b/worker/loop.ts new file mode 100644 index 0000000..b142c80 --- /dev/null +++ b/worker/loop.ts @@ -0,0 +1,186 @@ +/** + * The runtime — the always-on cycle that makes the swarm real. + * + * Per tick, for each coin: real candles → real TA → 4 agents vote → verdict + * (all reused verbatim from src/lib/*), then the paper engine opens/closes + * positions and the Telegram layer broadcasts every fill. State is persisted + * each tick so it survives restarts. + * + * pnpm worker # run forever (default) + * pnpm worker:once # single tick, then exit (smoke test) + */ +import { getCandles, getMarketStats, type MarketStat } from '../src/lib/hyperliquid.ts' +import { summarizeTA, type TASnapshot } from '../src/lib/ta.ts' +import { screen } from '../src/lib/signals.ts' +import { CONFIG } from './config.ts' +import { load, save, getState } from './store.ts' +import { tgSend, tgPoll, TG_ENABLED } from './telegram.ts' +import { markToMarket, maybeOpen, recordSignal, stats, type Stats } from './paper.ts' +import type { Position, Trade } from './store.ts' + +// ── formatting ──────────────────────────────────────────────────────────────── +const usd = (n: number) => + '$' + n.toLocaleString('en-US', { maximumFractionDigits: n >= 100 ? 0 : 2 }) +const px = (n: number) => + '$' + n.toLocaleString('en-US', { maximumFractionDigits: n >= 100 ? 0 : n >= 1 ? 2 : 4 }) +const pct = (n: number) => (n >= 0 ? '+' : '') + n.toFixed(2) + '%' +const signed = (n: number) => (n >= 0 ? '+' : '−') + usd(Math.abs(n)).slice(1) + +const lastDecision = new Map() + +// ── telegram messages ─────────────────────────────────────────────────────── +function openMsg(p: Position): string { + const dot = p.dir === 'LONG' ? '🟢' : '🔴' + return [ + `${dot} ${p.dir} ${p.coin}-PERP @ ${px(p.entry)}`, + `size ${(p.notional / getState().equity / CONFIG.leverage * 100).toFixed(1)}% · ×${CONFIG.leverage} → ${usd(p.notional)} notional`, + `TP ${pct(p.tpPct)} (${px(p.tp)}) · SL ${pct(-p.slPct)} (${px(p.sl)})`, + `confidence ${Math.round(p.confidence * 100)}% · ${p.reason}`, + ].join('\n') +} + +function closeMsg(t: Trade): string { + const mark = t.outcome === 'tp' ? '✅ TP' : '❌ SL' + return [ + `${mark} ${t.coin}-PERP ${t.dir}`, + `${px(t.entry)} → ${px(t.exit)} (${pct(t.pnlPct)}, ${signed(t.pnlUsd)})`, + `held ${t.holdMin >= 60 ? (t.holdMin / 60).toFixed(1) + 'h' : t.holdMin + 'm'} · equity ${usd(getState().equity)}`, + ].join('\n') +} + +function statusMsg(): string { + const st = getState() + const s = stats() + const open = st.positions.length + ? st.positions.map((p) => `${p.coin} ${p.dir}`).join(' · ') + : '—' + return [ + `📊 $QUANT — paper · ${CONFIG.mode}`, + `equity ${usd(s.equity)} (${pct(s.roiPct)})`, + `open ${open}`, + `closed ${s.trades} · win ${Math.round(s.winRate * 100)}%`, + ].join('\n') +} + +function learnMsg(): string { + const s = stats() + if (!s.trades) return '🧠 Retrospective\nNo closed trades yet — be patient.' + const pf = s.profitFactor === Infinity ? '∞' : s.profitFactor.toFixed(2) + const lines = [ + `🧠 Retrospective — ${s.trades} trades`, + `win rate ${Math.round(s.winRate * 100)}% · profit factor ${pf}`, + `ROI ${pct(s.roiPct)} (${signed(s.pnlUsd)})`, + `avg win ${signed(s.avgWin)} · avg loss ${signed(-s.avgLoss)}`, + ] + if (s.best) lines.push(`best ${signed(s.best.pnlUsd)} (${s.best.coin} ${s.best.dir})`) + if (s.worst) lines.push(`worst ${signed(s.worst.pnlUsd)} (${s.worst.coin} ${s.worst.dir})`) + return lines.join('\n') +} + +function positionsMsg(): string { + const st = getState() + if (!st.positions.length) return '📭 No open positions.' + return ( + '📂 Open positions\n' + + st.positions + .map( + (p) => + `${p.dir === 'LONG' ? '🟢' : '🔴'} ${p.coin} ${p.dir} @ ${px(p.entry)} · TP ${px(p.tp)} / SL ${px(p.sl)}`, + ) + .join('\n') + ) +} + +const helpMsg = () => + [ + '🤖 $QUANT paper agent', + '/status — equity, open positions, win rate', + '/positions — open positions detail', + '/pnl or /learn — retrospective over closed trades', + '/help — this message', + ].join('\n') + +// ── core tick ───────────────────────────────────────────────────────────────── +async function tickCoin(coin: string, stat: MarketStat | null): Promise { + const ctrl = new AbortController() + const t = setTimeout(() => ctrl.abort(), 12_000) + try { + const candles = await getCandles(coin, CONFIG.interval, CONFIG.lookback, ctrl.signal) + const ta: TASnapshot = summarizeTA(candles, stat?.funding ?? 0) + const { verdict } = screen(ta, stat ?? undefined) + const price = stat?.mark ?? ta.price + + // 1) manage open positions against the real price + for (const tr of markToMarket(coin, price)) await tgSend(closeMsg(tr)) + + // 2) open a fresh position when the swarm has an edge + const opened = maybeOpen(coin, price, verdict) + if (opened) await tgSend(openMsg(opened)) + + // 3) log the call only when the decision flips (keeps the feed meaningful) + if (verdict.decision !== lastDecision.get(coin)) { + lastDecision.set(coin, verdict.decision) + recordSignal({ + t: Date.now(), + coin, + decision: verdict.decision, + confidence: verdict.confidence, + price, + net: verdict.net, + reason: verdict.reasoning, + }) + } + console.log( + `[${coin}] ${px(price)} ${verdict.decision.padEnd(5)} conf ${verdict.confidence.toFixed(2)} net ${verdict.net.toFixed(2)}`, + ) + } catch (e) { + console.error(`[${coin}] tick error:`, (e as Error).message) + } finally { + clearTimeout(t) + } +} + +async function tick(): Promise { + let byCoin = new Map() + try { + const all = await getMarketStats(CONFIG.coins) + byCoin = new Map(all.map((s) => [s.coin, s])) + } catch (e) { + console.error('[stats] fetch failed:', (e as Error).message) + } + for (const coin of CONFIG.coins) await tickCoin(coin, byCoin.get(coin) ?? null) + save() +} + +async function commandLoop(): Promise { + await tgPoll(async (cmd, chatId) => { + if (cmd === '/status' || cmd === '/start') await tgSend(statusMsg(), chatId) + else if (cmd === '/pnl' || cmd === '/learn') await tgSend(learnMsg(), chatId) + else if (cmd === '/positions') await tgSend(positionsMsg(), chatId) + else if (cmd === '/help') await tgSend(helpMsg(), chatId) + }) +} + +// ── boot ────────────────────────────────────────────────────────────────────── +function bootMsg(s: Stats): string { + return [ + `🟩 $QUANT paper agent online — ${CONFIG.mode}`, + `${CONFIG.coins.join(' · ')} · ${CONFIG.interval} · poll ${CONFIG.pollMs / 1000}s`, + `equity ${usd(s.equity)} · ${s.trades} trades so far`, + ].join('\n') +} + +async function main(): Promise { + const once = process.argv.includes('--once') + load() + console.log( + `QUANT paper worker · mode ${CONFIG.mode} · coins ${CONFIG.coins.join(',')} · interval ${CONFIG.interval} · TG ${TG_ENABLED ? 'on' : 'off'}${once ? ' · ONCE' : ''}`, + ) + await tgSend(bootMsg(stats())) + await tick() + if (once) return + setInterval(tick, CONFIG.pollMs) + if (CONFIG.tgToken) setInterval(commandLoop, 3_000) +} + +main() diff --git a/worker/paper.ts b/worker/paper.ts new file mode 100644 index 0000000..14a19e3 --- /dev/null +++ b/worker/paper.ts @@ -0,0 +1,133 @@ +/** + * Paper-trading engine. Turns the (real) verdict into (paper) positions and marks + * them to market against (real) Hyperliquid prices — so the PnL track record is + * honest and verifiable, not seeded. One position per coin; TP/SL assumed filled + * at the level. This is the layer that makes the LARP's "performance" real. + */ +import { getState, type Position, type Trade, type SignalLog } from './store.ts' +import { CONFIG } from './config.ts' +import type { Verdict } from '../src/lib/signals.ts' + +const round = (n: number, d = 2) => Math.round(n * 10 ** d) / 10 ** d +const sum = (a: number[]) => a.reduce((s, x) => s + x, 0) + +let seq = 0 +const newId = (coin: string) => `${coin}-${Date.now().toString(36)}-${(seq++).toString(36)}` + +/** Close any open position whose TP/SL the current price has crossed. */ +export function markToMarket(coin: string, price: number): Trade[] { + const st = getState() + const closed: Trade[] = [] + const keep: Position[] = [] + for (const p of st.positions) { + if (p.coin !== coin) { + keep.push(p) + continue + } + const hitTp = p.dir === 'LONG' ? price >= p.tp : price <= p.tp + const hitSl = p.dir === 'LONG' ? price <= p.sl : price >= p.sl + if (hitTp || hitSl) { + closed.push(close(p, hitTp ? p.tp : p.sl, hitTp ? 'tp' : 'sl')) + } else { + keep.push(p) + } + } + st.positions = keep + return closed +} + +function close(p: Position, exit: number, outcome: Trade['outcome']): Trade { + const st = getState() + const move = (exit - p.entry) / p.entry + const pnlPct = (p.dir === 'LONG' ? move : -move) * 100 + const pnlUsd = p.notional * (pnlPct / 100) + st.equity = round(st.equity + pnlUsd) + const trade: Trade = { + ...p, + exit, + closedAt: Date.now(), + pnlPct: round(pnlPct, 3), + pnlUsd: round(pnlUsd), + outcome, + holdMin: Math.round((Date.now() - p.openedAt) / 60_000), + } + st.closed.push(trade) + if (st.closed.length > 500) st.closed.splice(0, st.closed.length - 500) + st.equityCurve.push({ t: trade.closedAt, equity: st.equity }) + if (st.equityCurve.length > 1000) st.equityCurve.splice(0, st.equityCurve.length - 1000) + return trade +} + +/** Open a paper position if the verdict is actionable and above the conf floor. */ +export function maybeOpen(coin: string, price: number, v: Verdict): Position | null { + const st = getState() + if (v.decision === 'SKIP') return null + if (v.confidence < CONFIG.confFloor) return null + if (st.positions.filter((p) => p.coin === coin).length >= CONFIG.maxOpenPerCoin) return null + + const dir = v.decision as Position['dir'] + const tp = dir === 'LONG' ? price * (1 + v.tpPct / 100) : price * (1 - v.tpPct / 100) + const sl = dir === 'LONG' ? price * (1 - v.slPct / 100) : price * (1 + v.slPct / 100) + const notional = round(st.equity * (v.sizePct / 100) * CONFIG.leverage) + + const pos: Position = { + id: newId(coin), + coin, + dir, + entry: price, + notional, + tp: round(tp, 4), + sl: round(sl, 4), + tpPct: v.tpPct, + slPct: v.slPct, + confidence: v.confidence, + reason: v.reasoning, + openedAt: Date.now(), + } + st.positions.push(pos) + return pos +} + +/** Append to the rolling signal log (call only when a coin's decision changes). */ +export function recordSignal(s: SignalLog): void { + const st = getState() + st.signals.push(s) + if (st.signals.length > 200) st.signals.splice(0, st.signals.length - 200) +} + +export type Stats = { + trades: number + open: number + winRate: number + pnlUsd: number + roiPct: number + profitFactor: number + avgWin: number + avgLoss: number + best: Trade | null + worst: Trade | null + equity: number +} + +/** Retrospective over closed trades — the honest, no-LLM version of /learn. */ +export function stats(): Stats { + const st = getState() + const c = st.closed + const wins = c.filter((t) => t.pnlUsd > 0) + const losses = c.filter((t) => t.pnlUsd <= 0) + const grossWin = sum(wins.map((t) => t.pnlUsd)) + const grossLoss = -sum(losses.map((t) => t.pnlUsd)) + return { + trades: c.length, + open: st.positions.length, + winRate: c.length ? wins.length / c.length : 0, + pnlUsd: round(st.equity - CONFIG.equityStart), + roiPct: round((st.equity / CONFIG.equityStart - 1) * 100, 2), + profitFactor: grossLoss ? round(grossWin / grossLoss) : grossWin > 0 ? Infinity : 0, + avgWin: wins.length ? round(grossWin / wins.length) : 0, + avgLoss: losses.length ? round(grossLoss / losses.length) : 0, + best: c.reduce((m, t) => (t.pnlUsd > (m?.pnlUsd ?? -Infinity) ? t : m), null), + worst: c.reduce((m, t) => (t.pnlUsd < (m?.pnlUsd ?? Infinity) ? t : m), null), + equity: round(st.equity), + } +} diff --git a/worker/store.ts b/worker/store.ts new file mode 100644 index 0000000..1754e23 --- /dev/null +++ b/worker/store.ts @@ -0,0 +1,103 @@ +/** + * Persistence — a single JSON file written atomically (tmp + rename). No native + * deps, survives restarts, and the exact shape a future `/api/*` endpoint would + * serve to make the site's Performance + fills honest. Swap for SQLite later + * without touching callers (the engine only talks to getState()/save()). + */ +import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from 'node:fs' +import { dirname } from 'node:path' +import { CONFIG } from './config.ts' +import type { Direction } from '../src/lib/signals.ts' + +export type Side = Exclude + +export type Position = { + id: string + coin: string + dir: Side + entry: number + notional: number // USD exposure (= equity * size% * leverage) + tp: number + sl: number + tpPct: number + slPct: number + confidence: number + reason: string + openedAt: number +} + +export type Trade = Position & { + exit: number + closedAt: number + pnlPct: number // direction-adjusted price move, % + pnlUsd: number // notional * pnlPct + outcome: 'tp' | 'sl' + holdMin: number +} + +export type SignalLog = { + t: number + coin: string + decision: Direction + confidence: number + price: number + net: number + reason: string +} + +export type State = { + startedAt: number + mode: string + equity: number + positions: Position[] + closed: Trade[] + signals: SignalLog[] + equityCurve: { t: number; equity: number }[] +} + +let state: State | null = null + +function fresh(): State { + return { + startedAt: Date.now(), + mode: CONFIG.mode, + equity: CONFIG.equityStart, + positions: [], + closed: [], + signals: [], + equityCurve: [{ t: Date.now(), equity: CONFIG.equityStart }], + } +} + +/** Load persisted state, or start fresh. Idempotent. */ +export function load(): State { + if (state) return state + try { + if (existsSync(CONFIG.dataFile)) { + const raw = JSON.parse(readFileSync(CONFIG.dataFile, 'utf8')) as State + state = { ...fresh(), ...raw } + return state + } + } catch (e) { + console.error('[store] load failed, starting fresh:', (e as Error).message) + } + state = fresh() + return state +} + +export function getState(): State { + return state ?? load() +} + +/** Atomic write: serialise to `.tmp`, then rename over the real file. */ +export function save(): void { + const st = getState() + try { + mkdirSync(dirname(CONFIG.dataFile), { recursive: true }) + const tmp = `${CONFIG.dataFile}.tmp` + writeFileSync(tmp, JSON.stringify(st, null, 2)) + renameSync(tmp, CONFIG.dataFile) + } catch (e) { + console.error('[store] save failed:', (e as Error).message) + } +} diff --git a/worker/telegram.ts b/worker/telegram.ts new file mode 100644 index 0000000..1d3e689 --- /dev/null +++ b/worker/telegram.ts @@ -0,0 +1,66 @@ +/** + * Telegram — the real L6 "Comms" layer, in ~60 lines via the raw Bot API (no + * dependency). Outbound: tgSend() broadcasts signals/closes. Inbound: tgPoll() + * long-polls getUpdates for slash commands (/status, /pnl, /positions, /help). + * + * If no token/chat is configured the worker still runs — messages print to stdout + * so you can demo the whole loop with zero Telegram setup. + */ +import { CONFIG } from './config.ts' + +const api = (method: string) => `https://api.telegram.org/bot${CONFIG.tgToken}/${method}` + +export const TG_ENABLED = Boolean(CONFIG.tgToken && CONFIG.tgChat) + +const stripHtml = (s: string) => s.replace(/<[^>]+>/g, '') + +/** Send a message (HTML). Defaults to the configured chat; pass chatId to reply. */ +export async function tgSend(text: string, chatId: string | number = CONFIG.tgChat): Promise { + if (!CONFIG.tgToken || !chatId) { + console.log('\n' + stripHtml(text) + '\n') + return + } + try { + const res = await fetch(api('sendMessage'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + chat_id: chatId, + text, + parse_mode: 'HTML', + disable_web_page_preview: true, + }), + }) + if (!res.ok) console.error('[tg] send failed', res.status, await res.text()) + } catch (e) { + console.error('[tg] send error', (e as Error).message) + } +} + +let offset = 0 + +/** Poll once for new commands and dispatch them. Cheap to call on an interval. */ +export async function tgPoll( + handler: (cmd: string, chatId: number) => Promise, +): Promise { + if (!CONFIG.tgToken) return + try { + const res = await fetch(api('getUpdates') + `?timeout=0&offset=${offset}`) + if (!res.ok) return + const json = (await res.json()) as { + result?: { update_id: number; message?: { text?: string; chat?: { id: number } } }[] + } + for (const u of json.result ?? []) { + offset = u.update_id + 1 + const text = u.message?.text + const chatId = u.message?.chat?.id + if (text && chatId != null && text.startsWith('/')) { + // strip @BotName suffix and arguments → bare command + const cmd = text.trim().split(/\s+/)[0].split('@')[0].toLowerCase() + await handler(cmd, chatId) + } + } + } catch { + /* transient — ignore, next poll retries */ + } +}