From 54be07e6cc4758ca10868fad29615b49baf49fe6 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 9 Apr 2026 23:27:06 -0700 Subject: [PATCH] wip --- .cursor/rules/convex-env.mdc | 45 +++++ CHANGELOG.md | 5 +- bun.lock | 130 ++++++++++--- components/ui/popover.tsx | 31 ++++ convex/actions.ts | 77 +++++--- convex/repoBuilds.ts | 30 +++ docs/github-actions-build-testing.md | 93 ++++++++++ package.json | 6 +- postcss.config.mjs | 5 + src/components/AutocompleteField.tsx | 76 -------- src/components/ComboboxField.tsx | 210 +++++++++++++++++++++ src/index.css | 7 +- src/lib/formatBuildErrorSummary.ts | 47 ++++- src/lib/repoTreeUrl.ts | 30 +++ src/pages/HomePage.tsx | 5 +- src/pages/RepoPage.tsx | 261 ++++++++++++++++----------- vite.config.ts | 5 +- 17 files changed, 825 insertions(+), 238 deletions(-) create mode 100644 .cursor/rules/convex-env.mdc create mode 100644 components/ui/popover.tsx create mode 100644 docs/github-actions-build-testing.md create mode 100644 postcss.config.mjs delete mode 100644 src/components/AutocompleteField.tsx create mode 100644 src/components/ComboboxField.tsx create mode 100644 src/lib/repoTreeUrl.ts diff --git a/.cursor/rules/convex-env.mdc b/.cursor/rules/convex-env.mdc new file mode 100644 index 0000000..3d53df3 --- /dev/null +++ b/.cursor/rules/convex-env.mdc @@ -0,0 +1,45 @@ +--- +description: Convex deployment environment variable names (MeshEnvy mesh-forge); values are secrets — never commit them. +alwaysApply: true +--- + +# Convex environment variables (this deployment) + +These names are configured in the Convex dashboard for this project. **Do not put real values in the repo or in rules.** + +## Auth (OAuth / Convex Auth) + +- `AUTH_GITHUB_ID` +- `AUTH_GITHUB_SECRET` +- `AUTH_GOOGLE_ID` +- `AUTH_GOOGLE_SECRET` +- `JWKS` +- `JWT_PRIVATE_KEY` + +## App / Convex Auth config + +- `SITE_URL` — canonical site URL (often used by auth / redirects; set in dashboard as provided) + +**Code note:** Convex code in this repo also reads `CONVEX_SITE_URL` for `auth.config.ts` (JWT domain) and `actions.ts` (GitHub Actions `convex_url` input). If something breaks around auth domain or workflow callbacks, confirm both dashboard vars match what Convex and the frontend expect. + +## CI / GitHub integration + +- `GITHUB_TOKEN` — GitHub API (repo scans, branches, dispatching `custom_build*.yml`) +- `CONVEX_ENV` — e.g. `dev` selects `custom_build_test.yml` for workflow dispatch +- `CONVEX_BUILD_TOKEN` — shared with GitHub Actions secret of the same name; bearer auth for `POST /ingest-repo-build` + +## R2 (firmware artifacts / signed downloads) + +- `R2_ACCESS_KEY_ID` +- `R2_SECRET_ACCESS_KEY` +- `R2_ACCOUNT_ID` +- `R2_BUCKET_NAME` +- `R2_ENDPOINT_URL` +- `R2_PUBLIC_URL` +- `R2_CLOUDFLARE_TOKEN` + +**Code note:** `convex/lib/r2.ts` currently uses `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET_NAME` and builds the S3 endpoint from account id. Extra R2-related vars may be used elsewhere or reserved for future use. + +## When editing Convex code + +Prefer reading env vars that already exist in dashboard; if you introduce a new required variable, document it here and in any operator-facing setup notes. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb754b..1a60fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Patch +- **Build:** Tailwind uses **`source(none)`** plus explicit **`@source`** globs for **`src/`**, **`components/`**, and **`index.html`** only so **`vendor/`** (~100k+ vendored files) is never scanned (previously made `vite build` look hung). +- **Repo URLs:** optional **`/tree//target/`** path (branch segments may contain `/`); **`/owner/repo`** with no branch shows **`--branch--`** / **`--target--`** placeholders until chosen. Target picker stays disabled until a branch is selected; URL updates with branch and target. +- **Repo page:** single-column layout shows **branch / target / Flash** above **About**; branch and target use a **Radix Popover combobox** (filter field + full list) instead of `` so arrow keys work after a selection. - **Repo About sidebar:** uses GitHub REST **`description`** and **`homepage`** (cached with branch list refresh), not a README-derived blurb—matches GitHub’s About section (text + link icon). Removed unused **`readmePlainSummary`** helper. - **Repo page layout:** GitHub-style **About** sidebar (**Refresh branches** under blurb; **GitHub** icon opens the ref tree next to homepage or repo title); main column **toolbar** (branch, target, **Flash** only), CI/USB, then **README** body (no separate “README” heading; spacing only above README so a leading `---` is not doubled with a border). Removed **device compatibility** voting (works / does not work) from the repo page. - **Repo README:** render inline HTML via `rehype-raw` + `rehype-sanitize` (badges, images) in the main column. -- **Repo builds:** show a short explanation when GitHub returns **422 unexpected `workflow_dispatch` inputs** (common when Mesh Forge’s workflow YAML on GitHub is behind this repo); still truncate other long errors. +- **Repo builds:** friendlier **CI failed** copy (headline + body + **Technical details** `
`); primary button **Retry build** re-queues via **`retryBuild`**; **Flash** disabled while **queued/running**; GitHub **dispatch** retries **4×** with backoff on **5xx/429** and common network errors. Links: **View run** when `githubRunId` set, else **Mesh Forge workflow** when failed with no run. Shorter **422 / unexpected inputs** maintainer copy in `formatBuildErrorSummary`. - **RepoPage:** treat missing scan row (`null`) like loading so the UI does not crash before `ensureScan` creates the document. - Removed in-app **documentation** routes (`/docs`, ESP/nRF markdown pages). **`/docs/*`** and legacy **`/flasher`** URLs redirect to **home**. ESP **Web Serial** flashing only on **`/:owner/:repo` / tree** pages: when a build **succeeds**, the signed bundle is fetched automatically and **`EspFlasher`** appears (no separate upload page). Flasher options: baud, full erase, **no auto-reset**, **1200 baud** bootloader pulse. Unknown paths show **not found** instead of a blank screen. - Fixed **RepoPage** Rules of Hooks violation (blank page after default-branch redirect) by running all hooks before any `Navigate` return. diff --git a/bun.lock b/bun.lock index 4b12ba7..e09928d 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@aws-sdk/client-s3": "^3.937.0", "@aws-sdk/s3-request-presigner": "^3.937.0", "@convex-dev/auth": "^0.0.90", + "@radix-ui/react-popover": "^1.1.15", "convex": "^1.29.3", "convex-helpers": "^0.1.106", "esptool-js": "^0.6.0", @@ -25,19 +26,20 @@ "@mdx-js/rollup": "^3.1.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-slot": "^1.2.4", + "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.17", "@types/node": "^24.10.1", "@types/pako": "^2.0.4", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "class-variance-authority": "^0.7.1", + "postcss": "^8.5.9", "prettier": "^3.7.3", "prettier-plugin-organize-imports": "^4.3.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.17", + "tailwindcss": "4.2.2", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "vite": "^7.2.6", @@ -46,6 +48,8 @@ }, }, "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@auth/core": ["@auth/core@0.37.4", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^5.9.6", "oauth4webapi": "^3.1.1", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], @@ -242,6 +246,14 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], @@ -312,28 +324,52 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], @@ -488,38 +524,38 @@ "@speed-highlight/core": ["@speed-highlight/core@1.2.12", "", {}, "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "dependencies": { "@emnapi/core": "^1.6.0", "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="], "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.17", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -562,6 +598,8 @@ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], "atob-lite": ["atob-lite@2.0.0", "", {}, "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw=="], @@ -624,11 +662,13 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "electron-to-chromium": ["electron-to-chromium@1.5.259", "", {}, "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ=="], - "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -674,6 +714,8 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -896,7 +938,7 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], @@ -918,10 +960,16 @@ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-router": ["react-router@7.14.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ=="], "react-router-dom": ["react-router-dom@7.14.0", "", { "dependencies": { "react-router": "7.14.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], @@ -982,7 +1030,7 @@ "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], - "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], + "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], @@ -1020,6 +1068,10 @@ "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -1056,17 +1108,21 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -1080,6 +1136,8 @@ "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "wrangler/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -1088,6 +1146,30 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx new file mode 100644 index 0000000..f4c0728 --- /dev/null +++ b/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as PopoverPrimitive from '@radix-ui/react-popover' +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'start', sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } diff --git a/convex/actions.ts b/convex/actions.ts index e0eff82..742e18f 100644 --- a/convex/actions.ts +++ b/convex/actions.ts @@ -39,28 +39,61 @@ export const dispatchRepoBuild = action({ }, } - try { - const url = `https://api.github.com/repos/MeshEnvy/mesh-forge/actions/workflows/${workflowFile}/dispatches` - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${githubToken}`, - Accept: "application/vnd.github.v3+json", - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }) - - if (!response.ok) { - const errorText = await response.text() - throw new Error(`GitHub API failed: ${response.status} ${errorText}`) - } - } catch (error) { - await ctx.runMutation(internal.repoBuilds.logBuildDispatchError, { - buildId: args.buildId, - message: String(error), - }) - throw error + const url = `https://api.github.com/repos/MeshEnvy/mesh-forge/actions/workflows/${workflowFile}/dispatches` + const headers = { + Authorization: `Bearer ${githubToken}`, + Accept: "application/vnd.github.v3+json", + "Content-Type": "application/json", } + + const maxAttempts = 4 + let lastError: unknown + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (attempt > 1) { + const delayMs = 800 * 2 ** (attempt - 2) + await new Promise(r => setTimeout(r, delayMs)) + } + try { + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(payload), + }) + const errorText = await response.text() + if (response.ok) { + return + } + const transient = + response.status === 429 || + (response.status >= 500 && response.status < 600) + if (transient && attempt < maxAttempts) { + lastError = new Error(`GitHub API failed: ${response.status} ${errorText}`) + continue + } + throw new Error(`GitHub API failed: ${response.status} ${errorText}`) + } catch (error) { + lastError = error + const msg = String(error) + const network = + msg.includes("fetch failed") || + msg.includes("ECONNRESET") || + msg.includes("ETIMEDOUT") || + msg.includes("ENOTFOUND") || + msg.includes("EAI_AGAIN") + if (network && attempt < maxAttempts) { + continue + } + await ctx.runMutation(internal.repoBuilds.logBuildDispatchError, { + buildId: args.buildId, + message: String(error), + }) + throw error + } + } + await ctx.runMutation(internal.repoBuilds.logBuildDispatchError, { + buildId: args.buildId, + message: String(lastError), + }) + throw lastError }, }) diff --git a/convex/repoBuilds.ts b/convex/repoBuilds.ts index caebb43..9a51600 100644 --- a/convex/repoBuilds.ts +++ b/convex/repoBuilds.ts @@ -107,3 +107,33 @@ export const logBuildDispatchError = internalMutation({ }) }, }) + +/** Re-queue a failed build (same Convex doc + webhook id) after dispatch or CI flakiness. */ +export const retryBuild = mutation({ + args: { buildId: v.id("repoBuilds") }, + handler: async (ctx, args) => { + const doc = await ctx.db.get(args.buildId) + if (!doc) { + throw new Error("Build not found") + } + if (doc.status !== "failed") { + throw new Error(`Cannot retry unless status is failed (got ${doc.status})`) + } + + const now = Date.now() + await ctx.db.replace(args.buildId, { + owner: doc.owner, + repo: doc.repo, + ref: doc.ref, + resolvedSourceSha: doc.resolvedSourceSha, + targetEnv: doc.targetEnv, + buildKey: doc.buildKey, + status: "queued", + startedAt: now, + updatedAt: now, + }) + + await ctx.scheduler.runAfter(0, api.actions.dispatchRepoBuild, { buildId: args.buildId }) + return { ok: true as const } + }, +}) diff --git a/docs/github-actions-build-testing.md b/docs/github-actions-build-testing.md new file mode 100644 index 0000000..1354489 --- /dev/null +++ b/docs/github-actions-build-testing.md @@ -0,0 +1,93 @@ +# Testing firmware builds with GitHub Actions + +Mesh Forge compiles firmware on **GitHub Actions** (PlatformIO), not inside Convex. Convex **dispatches** a workflow and receives **status callbacks** over HTTP. + +Workflow files live in this repo under [`.github/workflows/`](../.github/workflows/): + +| Convex `CONVEX_ENV` | Workflow file | Typical use | +| ------------------- | ------------------------- | ------------- | +| `dev` | `custom_build_test.yml` | Local / dev | +| anything else | `custom_build.yml` | Production | + +Both workflows are **`workflow_dispatch` only** (no automatic runs on push). + +--- + +## 1. Convex environment variables + +Set these in the Convex dashboard for the deployment you are testing. **Names only** — never commit values. + +See [`.cursor/rules/convex-env.mdc`](../.cursor/rules/convex-env.mdc) for the full list used on your deployment. + +**Critical for this flow:** + +| Variable | Role | +| ---------------------- | -------------------------------------------------------------------- | +| `GITHUB_TOKEN` | Dispatches workflows on `MeshEnvy/mesh-forge` via GitHub REST API | +| `CONVEX_SITE_URL` | Passed to Actions as `convex_url` for `/ingest-repo-build` callbacks | +| `CONVEX_BUILD_TOKEN` | Must match the GitHub Actions secret of the same name (Bearer auth) | +| `CONVEX_ENV` | Set to `dev` to use `custom_build_test.yml` | + +If the dashboard has `SITE_URL` but not `CONVEX_SITE_URL`, set **`CONVEX_SITE_URL`** to your Convex **site** URL (the one that serves HTTP actions such as `/ingest-repo-build`). Auth (`auth.config.ts`) also uses `CONVEX_SITE_URL` for the JWT domain. + +--- + +## 2. GitHub repository secrets + +In **GitHub → `MeshEnvy/mesh-forge` → Settings → Secrets and variables → Actions**, the workflow needs: + +| Secret | Role | +| ------------------------ | ----------------------------------------- | +| `CONVEX_BUILD_TOKEN` | Same string as Convex `CONVEX_BUILD_TOKEN` | +| `CLOUDFLARE_ACCOUNT_ID` | Wrangler R2 upload | +| `CLOUDFLARE_API_TOKEN` | Wrangler R2 upload | +| `R2_BUCKET_NAME` | Object key prefix / bucket for artifacts | + +The workflow installs PlatformIO, builds, tars `.pio/build/`, then runs `bunx wrangler r2 object put ...`. + +--- + +## 3. GitHub token permissions + +The Convex `GITHUB_TOKEN` must be allowed to **trigger workflow dispatches** on `MeshEnvy/mesh-forge` (fine-grained PAT or classic PAT with appropriate `actions` scope). If dispatch fails, Convex may record a failure on the build row; check Convex function logs for HTTP `403` / `404` from `api.github.com`. + +--- + +## 4. End-to-end test (recommended) + +1. Deploy or run **`bunx convex dev`** with the env vars above set for that deployment. +2. Run the frontend (**`bun run dev`**) pointed at the same Convex deployment. +3. Open a repo page, pick a **branch** and **PlatformIO target**, and start a build (the app calls `ensureBuild`, which schedules `dispatchRepoBuild`). +4. In GitHub: **Actions** → workflow **“Repo PlatformIO Build”** → confirm a new run appears (`custom_build.yml` or `custom_build_test.yml` depending on `CONVEX_ENV`). +5. When the job finishes: + - **Success:** Convex build status should become **succeeded**, with `githubRunId` and R2 key; the UI can offer download / flash. + - **Failure:** Convex should get **failed** with an error summary from the workflow. + +**View run:** the repo page links to `https://github.com/MeshEnvy/mesh-forge/actions/runs/` when `githubRunId` is set. + +--- + +## 5. Manual `workflow_dispatch` (optional) + +You can run the workflow from **Actions → Repo PlatformIO Build → Run workflow** and fill inputs by hand. For callbacks to update the **correct** Convex document, **`repo_build_id`** must be a real `repoBuilds` document id from that Convex deployment (copy from dashboard or from a build started in the UI). + +You must still pass **`convex_url`** matching the deployment that owns that build id, and **`CONVEX_BUILD_TOKEN`** in GitHub must match that deployment. + +--- + +## 6. Troubleshooting + +| Symptom | Likely cause | +| ----------------------------------------------- | ---------------------------------------------------------------------------- | +| No workflow run after starting a build | Missing/invalid `GITHUB_TOKEN`, wrong repo, or API error (check Convex logs) | +| Workflow runs but UI stays queued / running | Wrong `convex_url`, or ingest `401` (token mismatch), or network block | +| Ingest returns 401 | `CONVEX_BUILD_TOKEN` differs between Convex and GitHub Actions | +| Build fails at R2 upload | Missing or wrong Cloudflare / R2 secrets | +| Wrong workflow file in dev | `CONVEX_ENV` not set to `dev` when you expect `custom_build_test.yml` | +| Source download fails | Private repo without token in workflow (current workflow uses public zip URL) | + +--- + +## 7. Changing the workflow + +After editing `custom_build_test.yml` (or `custom_build.yml`), merge to the branch GitHub uses for `ref` in the dispatch payload. Today Convex sends **`ref: "main"`** in the API body, so the workflow definition that runs is **`main`** on `MeshEnvy/mesh-forge`, not necessarily the branch you are building for the firmware repo. diff --git a/package.json b/package.json index bb69609..afc94d1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@aws-sdk/client-s3": "^3.937.0", "@aws-sdk/s3-request-presigner": "^3.937.0", "@convex-dev/auth": "^0.0.90", + "@radix-ui/react-popover": "^1.1.15", "convex": "^1.29.3", "convex-helpers": "^0.1.106", "esptool-js": "^0.6.0", @@ -42,19 +43,20 @@ "@mdx-js/rollup": "^3.1.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-slot": "^1.2.4", + "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.17", "@types/node": "^24.10.1", "@types/pako": "^2.0.4", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "class-variance-authority": "^0.7.1", + "postcss": "^8.5.9", "prettier": "^3.7.3", "prettier-plugin-organize-imports": "^4.3.0", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.4.0", - "tailwindcss": "^4.1.17", + "tailwindcss": "4.2.2", "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "vite": "^7.2.6", diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..a7f73a2 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/src/components/AutocompleteField.tsx b/src/components/AutocompleteField.tsx deleted file mode 100644 index daf77f1..0000000 --- a/src/components/AutocompleteField.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useId } from 'react' - -type AutocompleteFieldProps = { - label: string - options: readonly string[] - value: string - onChange: (value: string) => void - onBlur?: () => void - disabled?: boolean - id?: string - placeholder?: string - /** Single-row toolbar: label left, input grows. */ - layout?: 'stacked' | 'inline' -} - -/** Native ``-backed field: typeahead from the browser + keyboard friendly. */ -export function AutocompleteField({ - label, - options, - value, - onChange, - onBlur, - disabled, - id, - placeholder = 'Type or pick from list…', - layout = 'stacked', -}: AutocompleteFieldProps) { - const rid = useId().replace(/:/g, '') - const inputId = id ?? `ac-${rid}` - const listId = `${inputId}-options` - - const inputClass = - layout === 'inline' - ? 'h-9 min-w-[7rem] flex-1 bg-slate-900 border border-slate-700 rounded-md px-2.5 text-sm text-white placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-cyan-600/50 disabled:cursor-not-allowed disabled:opacity-50' - : 'w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2.5 text-sm text-white placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-cyan-600/50 disabled:cursor-not-allowed disabled:opacity-50' - - return ( - - ) -} diff --git a/src/components/ComboboxField.tsx b/src/components/ComboboxField.tsx new file mode 100644 index 0000000..d5fe984 --- /dev/null +++ b/src/components/ComboboxField.tsx @@ -0,0 +1,210 @@ +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { cn } from '@/lib/utils' +import { ChevronDown } from 'lucide-react' +import { useEffect, useId, useMemo, useRef, useState } from 'react' + +type Row = { kind: 'clear' } | { kind: 'opt'; value: string } + +type ComboboxFieldProps = { + label: string + options: readonly string[] + value: string + onChange: (value: string) => void + disabled?: boolean + id?: string + placeholder?: string + layout?: 'stacked' | 'inline' + /** When set and `value` is non-empty, first row clears selection (`onChange('')`). */ + clearSelectionLabel?: string +} + +function buildRows( + options: readonly string[], + filter: string, + clearSelectionLabel: string | undefined, + value: string +): Row[] { + const q = filter.trim().toLowerCase() + const filtered = !q ? [...options] : options.filter(o => o.toLowerCase().includes(q)) + const r: Row[] = [] + if (clearSelectionLabel && value) r.push({ kind: 'clear' }) + for (const o of filtered) r.push({ kind: 'opt', value: o }) + return r +} + +/** + * Searchable list combobox (Radix Popover). Unlike ``, the full option list stays + * available while open so arrow keys and scrolling work after a value is selected. + */ +export function ComboboxField({ + label, + options, + value, + onChange, + disabled, + id, + placeholder = 'Choose…', + layout = 'stacked', + clearSelectionLabel, +}: ComboboxFieldProps) { + const rid = useId().replace(/:/g, '') + const triggerId = id ?? `cb-${rid}` + const labelId = `${triggerId}-label` + const [open, setOpen] = useState(false) + const [filter, setFilter] = useState('') + const [highlighted, setHighlighted] = useState(0) + const filterInputRef = useRef(null) + const listRef = useRef(null) + + const rows = useMemo( + () => buildRows(options, filter, clearSelectionLabel, value), + [options, filter, clearSelectionLabel, value] + ) + + const handleOpenChange = (next: boolean) => { + if (next) { + setFilter('') + const initial = buildRows(options, '', clearSelectionLabel, value) + const idx = initial.findIndex(row => row.kind === 'opt' && row.value === value) + setHighlighted(idx >= 0 ? idx : 0) + } else { + setFilter('') + } + setOpen(next) + } + + useEffect(() => { + if (!open) return + setHighlighted(h => Math.min(h, Math.max(0, rows.length - 1))) + }, [rows.length, open]) + + useEffect(() => { + if (!open) return + requestAnimationFrame(() => filterInputRef.current?.focus()) + }, [open]) + + useEffect(() => { + if (!open || !listRef.current) return + const el = listRef.current.querySelector(`[data-row-index="${highlighted}"]`) + el?.scrollIntoView({ block: 'nearest' }) + }, [highlighted, open, rows.length]) + + const selectRow = (row: Row) => { + if (row.kind === 'clear') { + onChange('') + } else { + onChange(row.value) + } + setFilter('') + setOpen(false) + } + + const onFilterKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault() + setHighlighted(h => Math.min(h + 1, Math.max(0, rows.length - 1))) + return + } + if (e.key === 'ArrowUp') { + e.preventDefault() + setHighlighted(h => Math.max(h - 1, 0)) + return + } + if (e.key === 'Enter' && rows.length > 0) { + e.preventDefault() + const row = rows[highlighted] + if (row) selectRow(row) + return + } + if (e.key === 'Escape') { + e.preventDefault() + setOpen(false) + } + } + + const triggerClass = + layout === 'inline' + ? 'h-9 min-w-[7rem] flex-1 rounded-md border border-slate-700 bg-slate-900 px-2.5 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-cyan-600/50' + : 'h-10 w-full rounded-md border border-slate-700 bg-slate-900 px-2.5 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-cyan-600/50' + + const labelWrapClass = + layout === 'inline' + ? 'flex min-w-0 max-w-[min(100%,18rem)] flex-1 items-center gap-2 sm:max-w-[20rem]' + : 'block w-full space-y-1.5' + + const labelTextClass = + layout === 'inline' ? 'w-14 shrink-0 text-xs font-medium text-slate-500 sm:w-16' : 'text-sm font-medium text-slate-400' + + return ( +
+ + {label} + + + + + + e.preventDefault()} + align="start" + > +
+ { + setFilter(e.target.value) + setHighlighted(0) + }} + onKeyDown={onFilterKeyDown} + placeholder="Filter…" + className="h-8 w-full rounded border border-slate-800 bg-slate-950 px-2 text-sm text-white placeholder:text-slate-600 focus:outline-none focus:ring-1 focus:ring-cyan-600/50" + autoComplete="off" + spellCheck={false} + /> +
+
    + {rows.length === 0 ? ( +
  • No matches
  • + ) : ( + rows.map((row, i) => ( +
  • setHighlighted(i)} + onMouseDown={e => e.preventDefault()} + onClick={() => selectRow(row)} + > + {row.kind === 'clear' ? ( + {clearSelectionLabel} + ) : ( + row.value + )} +
  • + )) + )} +
+
+
+
+ ) +} diff --git a/src/index.css b/src/index.css index cf98657..1674fb7 100644 --- a/src/index.css +++ b/src/index.css @@ -1,6 +1,11 @@ -@import "tailwindcss"; +/* Disable auto scan of the whole repo root — `vendor/` alone is ~100k+ files and stalls the Oxide scanner. */ +@import "tailwindcss" source(none); @plugin "@tailwindcss/typography"; +@source "./**/*.{html,js,mjs,mts,ts,tsx,jsx}"; +@source "../components/**/*.{html,js,mjs,mts,ts,tsx,jsx}"; +@source "../index.html"; + @theme { --font-sans: system-ui, -apple-system, sans-serif; diff --git a/src/lib/formatBuildErrorSummary.ts b/src/lib/formatBuildErrorSummary.ts index 1961e3e..71ae39e 100644 --- a/src/lib/formatBuildErrorSummary.ts +++ b/src/lib/formatBuildErrorSummary.ts @@ -3,9 +3,8 @@ export function formatBuildErrorSummary(summary: string | undefined): string { if (!summary) return '' if (summary.includes('Unexpected inputs provided')) { return ( - 'GitHub Actions rejected this workflow dispatch: the workflow file on the Mesh Forge GitHub repo ' + - 'does not declare the same `workflow_dispatch` inputs as this app (update `custom_build.yml` / ' + - '`custom_build_test.yml` on `MeshEnvy/mesh-forge` and try again).' + 'Mesh Forge’s own GitHub workflow didn’t accept the build request (inputs out of sync). ' + + 'That’s on the Mesh Forge service, not your firmware. Retry won’t help until that workflow YAML is updated.' ) } if (summary.length > 600) { @@ -13,3 +12,45 @@ export function formatBuildErrorSummary(summary: string | undefined): string { } return summary } + +/** Short headline + body for people browsing firmware, not operating Mesh Forge. */ +export function buildFailurePresentation(summary: string | undefined): { + headline: string + body: string +} { + if (!summary?.trim()) { + return { headline: 'Build did not finish.', body: '' } + } + if (summary.includes('Unexpected inputs provided')) { + return { + headline: 'Couldn’t start a cloud build', + body: + 'Something on the Mesh Forge side didn’t line up with GitHub. It’s not a signal that your repo is broken. ' + + 'Retry usually won’t fix this until the service is updated.', + } + } + if (/GitHub API failed:\s*5\d\d/.test(summary) || /GitHub API failed:\s*429/.test(summary)) { + return { + headline: 'GitHub was temporarily unavailable', + body: 'Starting the build failed because GitHub returned an error or rate limit. Use Retry build — it often works on a second try.', + } + } + if (/GitHub API failed:\s*4\d\d/.test(summary) && !summary.includes('422')) { + return { + headline: 'Couldn’t start the build', + body: formatBuildErrorSummary(summary), + } + } + if (summary.includes('GitHub API failed: 422')) { + return { + headline: 'Couldn’t start the build', + body: formatBuildErrorSummary(summary), + } + } + return { + headline: 'Build failed in CI', + body: + 'Often a compile error, missing PlatformIO dependency, or bad env config in the repo. Fix the project if you can, then use Retry build. ' + + 'Transient CI issues also happen — retry is safe.', + } +} diff --git a/src/lib/repoTreeUrl.ts b/src/lib/repoTreeUrl.ts new file mode 100644 index 0000000..2d147a5 --- /dev/null +++ b/src/lib/repoTreeUrl.ts @@ -0,0 +1,30 @@ +/** + * Mesh Forge tree URLs: `/owner/repo/tree//target/` + * Branch ref may contain `/`; `target` is a reserved final segment pair. + */ +const TARGET_TAIL = /\/target\/([^/]+)$/ + +export function parseTreeSplat(treePath: string | undefined): { + branchRef: string | null + targetEnv: string | null +} { + if (!treePath?.trim()) return { branchRef: null, targetEnv: null } + const segments = treePath.split('/').filter(Boolean) + if (segments.length === 0) return { branchRef: null, targetEnv: null } + const joined = segments.map(s => decodeURIComponent(s)).join('/') + const m = TARGET_TAIL.exec(joined) + if (!m) return { branchRef: joined, targetEnv: null } + const branchRef = joined.slice(0, m.index).replace(/\/$/, '') || null + const targetEnv = m[1] ? decodeURIComponent(m[1]) : null + return { branchRef, targetEnv } +} + +/** Path after `/tree/` (no leading slash). Empty string means no branch — use short repo URL. */ +export function buildTreeSplatPath(branchRef: string | null, targetEnv: string | null): string { + const b = branchRef?.trim() + if (!b) return '' + const enc = b.split('/').map(s => encodeURIComponent(s)).join('/') + const t = targetEnv?.trim() + if (!t) return enc + return `${enc}/target/${encodeURIComponent(t)}` +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 3ef252e..c57590f 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -106,8 +106,9 @@ export default function HomePage() {

- URLs mirror GitHub: /owner/repo/tree/ref. Short form{' '} - owner/repo uses the default branch. + Repo URLs: /owner/repo (pick branch),{' '} + /owner/repo/tree/ref, or add{' '} + /target/envName to deep-link a PlatformIO environment.

diff --git a/src/pages/RepoPage.tsx b/src/pages/RepoPage.tsx index 1497506..793186d 100644 --- a/src/pages/RepoPage.tsx +++ b/src/pages/RepoPage.tsx @@ -1,4 +1,4 @@ -import { AutocompleteField } from '../components/AutocompleteField' +import { ComboboxField } from '../components/ComboboxField' import EspFlasher from '../components/EspFlasher' import { Button } from '@/components/ui/button' import { api } from '@/convex/_generated/api' @@ -7,13 +7,17 @@ import { Github, Link2, RefreshCw } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { homepageHref, homepageLabel } from '../lib/githubHomepage' import ReactMarkdown from 'react-markdown' -import { Link, Navigate, useNavigate, useParams } from 'react-router-dom' +import { Link, useNavigate, useParams } from 'react-router-dom' import rehypeRaw from 'rehype-raw' import rehypeSanitize from 'rehype-sanitize' import remarkGfm from 'remark-gfm' import { toast } from 'sonner' import { normalizeBuildKey } from '../lib/buildKey' -import { formatBuildErrorSummary } from '../lib/formatBuildErrorSummary' +import { buildFailurePresentation } from '../lib/formatBuildErrorSummary' + +const MESH_FORGE_ACTIONS_REPO = 'MeshEnvy/mesh-forge' +const meshForgeWorkflowUrl = `https://github.com/${MESH_FORGE_ACTIONS_REPO}/actions/workflows/custom_build.yml` +import { buildTreeSplatPath, parseTreeSplat } from '../lib/repoTreeUrl' export default function RepoPage() { const navigate = useNavigate() @@ -23,7 +27,8 @@ export default function RepoPage() { const treePath = params['*'] const owner = useMemo(() => decodeURIComponent(ownerParam), [ownerParam]) const repo = useMemo(() => decodeURIComponent(repoParam), [repoParam]) - const onShortUrl = !treePath + const { branchRef, targetEnv: targetFromUrl } = useMemo(() => parseTreeSplat(treePath), [treePath]) + const hasBranch = Boolean(branchRef) const branchData = useQuery( api.repoBranches.get, @@ -34,17 +39,10 @@ export default function RepoPage() { const fetchReadme = useAction(api.repoBranches.fetchReadme) const ensureScan = useMutation(api.repoScans.ensureScan) const ensureBuild = useMutation(api.repoBuilds.ensureBuild) + const retryBuild = useMutation(api.repoBuilds.retryBuild) const getSignedUrl = useAction(api.repoBuildDownloads.getSignedDownloadUrl) - const effectiveRef = useMemo(() => { - if (treePath) { - return treePath - .split('/') - .filter(Boolean) - .map(p => decodeURIComponent(p)) - .join('/') - } - return branchData?.row?.defaultBranch ?? null - }, [treePath, branchData?.row?.defaultBranch]) + /** Git branch/ref from URL only (null = `--branch--` / short `/owner/repo`). */ + const effectiveRef = branchRef useEffect(() => { if (!owner || !repo || branchData === undefined) return @@ -101,27 +99,35 @@ export default function RepoPage() { }, [owner, repo, effectiveRef, fetchReadme]) const envNames = scan?.scanStatus === 'complete' ? scan.envNames ?? [] : [] - const [selectedEnv, setSelectedEnv] = useState('') - useEffect(() => { - if (!envNames.length) return - if (!selectedEnv || !envNames.includes(selectedEnv)) { - setSelectedEnv(envNames[0]) - } - }, [envNames, selectedEnv]) + const resolvedTargetEnv = + hasBranch && targetFromUrl && envNames.length > 0 && envNames.includes(targetFromUrl) + ? targetFromUrl + : '' const [branchDraft, setBranchDraft] = useState('') useEffect(() => { - if (!effectiveRef) return - setBranchDraft(effectiveRef) - }, [effectiveRef]) + setBranchDraft(branchRef ?? '') + }, [branchRef]) const [envDraft, setEnvDraft] = useState('') useEffect(() => { - if (selectedEnv) setEnvDraft(selectedEnv) - }, [selectedEnv]) + if (!hasBranch) { + setEnvDraft('') + return + } + setEnvDraft(targetFromUrl ?? '') + }, [hasBranch, targetFromUrl]) + + useEffect(() => { + if (!branchRef || !targetFromUrl) return + if (scan?.scanStatus !== 'complete') return + if (envNames.length === 0 || !envNames.includes(targetFromUrl)) { + navigate(`/${ownerParam}/${repoParam}/tree/${buildTreeSplatPath(branchRef, null)}`, { replace: true }) + } + }, [branchRef, targetFromUrl, envNames, scan?.scanStatus, navigate, ownerParam, repoParam]) const buildKey = - resolvedSha && selectedEnv ? normalizeBuildKey(resolvedSha, selectedEnv) : null + resolvedSha && resolvedTargetEnv ? normalizeBuildKey(resolvedSha, resolvedTargetEnv) : null const build = useQuery(api.repoBuilds.getByBuildKey, buildKey ? { buildKey } : 'skip') const [flashUrl, setFlashUrl] = useState(null) @@ -153,13 +159,19 @@ export default function RepoPage() { }, [build?._id, build?.status, getSignedUrl]) const queueFlashArtifacts = () => { - if (!effectiveRef || !resolvedSha || !selectedEnv) return + if (!effectiveRef || !resolvedSha || !resolvedTargetEnv) return + if (build?.status === 'failed' && build._id) { + void retryBuild({ buildId: build._id }) + .then(() => toast.message('Re-queued build')) + .catch(e => toast.error(String(e))) + return + } void ensureBuild({ owner, repo, ref: effectiveRef, resolvedSourceSha: resolvedSha, - targetEnv: selectedEnv, + targetEnv: resolvedTargetEnv, }).catch(e => toast.error(String(e))) } @@ -173,103 +185,108 @@ export default function RepoPage() { } } - if (onShortUrl) { - if (branchData === undefined) { - return ( -
- Resolving default branch… -
- ) - } - if (!branchData.row) { - return ( -
-

Could not load branch list. The repository may be private or missing.

- -
- ) - } - const enc = branchData.row.defaultBranch.split('/').map(encodeURIComponent).join('/') - return - } - - if (!effectiveRef) { + if (branchData === undefined) { return ( -
Loading…
+
Loading repository…
+ ) + } + if (!branchData.row) { + return ( +
+

Could not load branch list. The repository may be private or missing.

+ +
) } - const ghTree = `https://github.com/${owner}/${repo}/tree/${effectiveRef.split('/').map(encodeURIComponent).join('/')}` + const ghRepoRoot = `https://github.com/${owner}/${repo}` + const ghTree = effectiveRef + ? `https://github.com/${owner}/${repo}/tree/${effectiveRef.split('/').map(encodeURIComponent).join('/')}` + : ghRepoRoot - const branchNames = branchData?.row?.branches.map(b => b.name) ?? [] + const branchNames = branchData.row.branches.map(b => b.name) let branchOptions = - effectiveRef && !branchNames.includes(effectiveRef) - ? [effectiveRef, ...branchNames] - : [...branchNames] - if (branchOptions.length === 0 && effectiveRef) branchOptions = [effectiveRef] + branchRef && !branchNames.includes(branchRef) ? [branchRef, ...branchNames] : [...branchNames] + if (branchOptions.length === 0 && branchRef) branchOptions = [branchRef] - const ghAboutDescription = branchData?.row?.description?.trim() ?? '' - const ghAboutHomepage = branchData?.row?.homepage?.trim() ?? '' + const ghAboutDescription = branchData.row.description?.trim() ?? '' + const ghAboutHomepage = branchData.row.homepage?.trim() ?? '' - const scanReady = Boolean(resolvedSha && scan?.scanStatus === 'complete' && envNames.length > 0) + const scanReady = Boolean(hasBranch && resolvedSha && scan?.scanStatus === 'complete' && envNames.length > 0) + const buildInProgress = Boolean(build && (build.status === 'queued' || build.status === 'running')) const flashPrimaryDisabled = + !hasBranch || !resolvedSha || Boolean(refError) || - !selectedEnv || - !envNames.includes(selectedEnv) || - !scanReady + !resolvedTargetEnv || + !envNames.includes(resolvedTargetEnv) || + !scanReady || + buildInProgress - const ghRepoRoot = `https://github.com/${owner}/${repo}` + const flashButtonLabel = + build?.status === 'failed' ? 'Retry build' : buildInProgress ? 'Building…' : 'Flash' - const targetPlaceholder = !resolvedSha - ? '…' - : scan == null || scan.scanStatus === 'in_progress' - ? 'Scanning…' - : scan.scanStatus === 'failed' - ? 'Scan failed' - : envNames.length === 0 - ? 'No targets' - : 'Pick env…' + const targetPlaceholder = !hasBranch + ? '--target--' + : !resolvedSha + ? '…' + : scan == null || scan.scanStatus === 'in_progress' + ? 'Scanning…' + : scan.scanStatus === 'failed' + ? 'Scan failed' + : envNames.length === 0 + ? 'No targets' + : '--target--' return (
-
+
- { setBranchDraft(v) - if (branchOptions.includes(v)) { - const enc = v.split('/').map(encodeURIComponent).join('/') - navigate(`/${ownerParam}/${repoParam}/tree/${enc}`) + if (v === '') { + navigate(`/${ownerParam}/${repoParam}`) + return + } + if (branchOptions.includes(v)) { + navigate(`/${ownerParam}/${repoParam}/tree/${buildTreeSplatPath(v, null)}`) } - }} - onBlur={() => { - if (!branchOptions.includes(branchDraft)) setBranchDraft(effectiveRef) }} disabled={branchOptions.length === 0} /> - {scanReady && envNames.length > 0 ? ( - 0 ? ( + { setEnvDraft(v) - if (envNames.includes(v)) setSelectedEnv(v) - }} - onBlur={() => { - if (!envNames.includes(envDraft)) setEnvDraft(selectedEnv) + if (!branchRef) return + if (v === '') { + navigate(`/${ownerParam}/${repoParam}/tree/${buildTreeSplatPath(branchRef, null)}`, { + replace: true, + }) + return + } + if (envNames.includes(v)) { + navigate(`/${ownerParam}/${repoParam}/tree/${buildTreeSplatPath(branchRef, v)}`) + } }} disabled={false} /> @@ -279,10 +296,10 @@ export default function RepoPage() { )} @@ -293,14 +310,14 @@ export default function RepoPage() { disabled={flashPrimaryDisabled} onClick={queueFlashArtifacts} > - Flash + {flashButtonLabel}
{branchData?.isStale ? Branch list may be stale. : null} {refError ? {refError} : null} - {!refError && !resolvedSha ? Resolving branch… : null} + {!refError && hasBranch && !resolvedSha ? Resolving branch… : null} {resolvedSha && (scan == null || scan.scanStatus === 'in_progress') ? ( Scanning PlatformIO… ) : null} @@ -318,16 +335,46 @@ export default function RepoPage() { {build.githubRunId ? ( - Workflow run + View run on GitHub + + ) : build.status === 'failed' ? ( + + Mesh Forge workflow on GitHub ) : null}
{build.status === 'failed' && build.errorSummary ? ( -

{formatBuildErrorSummary(build.errorSummary)}

+
+ {(() => { + const { headline, body } = buildFailurePresentation(build.errorSummary) + return ( + <> +

{headline}

+ {body ?

{body}

: null} +
+ + Technical details + +
+                                {build.errorSummary.length > 2500
+                                  ? `${build.errorSummary.slice(0, 2500)}…`
+                                  : build.errorSummary}
+                              
+
+ + ) + })()} +
) : null} {build.status === 'succeeded' ? (
- {readmeMd === null ? ( + {!effectiveRef ? ( +

Select a branch to load the README.

+ ) : readmeMd === null ? (

Loading…

) : ( @@ -366,7 +415,7 @@ export default function RepoPage() {
-
) : null} diff --git a/vite.config.ts b/vite.config.ts index 5d817a7..0fb7dc3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,11 @@ -import tailwindcss from "@tailwindcss/vite" import react from "@vitejs/plugin-react" import path from "node:path" import { defineConfig } from "vite" +/** Tailwind runs via PostCSS (`postcss.config.mjs`), not `@tailwindcss/vite` — the Vite plugin was stalling builds (no stdout) on large workspaces. */ export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react()], + logLevel: "info", resolve: { alias: { "@": path.resolve(__dirname, "."),