mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-05-08 22:34:50 +02:00
wip
This commit is contained in:
@@ -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.
|
||||
+4
-1
@@ -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/<branch>/target/<env>`** 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 `<datalist>` 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** `<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.
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = 'start', sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-md border border-slate-700 bg-slate-900 p-0 text-slate-200 shadow-lg outline-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
|
||||
+55
-22
@@ -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
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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/<target_env>`, 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/<id>` 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.
|
||||
+4
-2
@@ -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",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
@@ -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 `<datalist>`-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 (
|
||||
<label
|
||||
className={
|
||||
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'
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
layout === 'inline'
|
||||
? 'shrink-0 text-xs font-medium text-slate-500 w-14 sm:w-16'
|
||||
: 'text-sm font-medium text-slate-400'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<input
|
||||
id={inputId}
|
||||
type="text"
|
||||
className={inputClass}
|
||||
list={options.length ? listId : undefined}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{options.length ? (
|
||||
<datalist id={listId}>
|
||||
{options.map(o => (
|
||||
<option key={o} value={o} />
|
||||
))}
|
||||
</datalist>
|
||||
) : null}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -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 `<datalist>`, 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<HTMLInputElement>(null)
|
||||
const listRef = useRef<HTMLUListElement>(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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className={labelWrapClass}>
|
||||
<span id={labelId} className={labelTextClass}>
|
||||
{label}
|
||||
</span>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
id={triggerId}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby={labelId}
|
||||
className={cn(triggerClass, 'inline-flex items-center justify-between gap-1 text-left font-normal')}
|
||||
>
|
||||
<span className={cn('min-w-0 truncate', !value && 'text-slate-600')}>{value || placeholder}</span>
|
||||
<ChevronDown className="size-4 shrink-0 opacity-60" aria-hidden />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] min-w-[12rem] p-0"
|
||||
onOpenAutoFocus={e => e.preventDefault()}
|
||||
align="start"
|
||||
>
|
||||
<div className="border-b border-slate-800 p-1.5">
|
||||
<input
|
||||
ref={filterInputRef}
|
||||
type="text"
|
||||
value={filter}
|
||||
onChange={e => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<ul ref={listRef} role="listbox" aria-label={label} className="max-h-60 overflow-y-auto py-1" tabIndex={-1}>
|
||||
{rows.length === 0 ? (
|
||||
<li className="px-2 py-2 text-sm text-slate-500">No matches</li>
|
||||
) : (
|
||||
rows.map((row, i) => (
|
||||
<li
|
||||
key={row.kind === 'clear' ? '__clear__' : row.value}
|
||||
role="option"
|
||||
aria-selected={i === highlighted}
|
||||
data-row-index={i}
|
||||
className={cn(
|
||||
'cursor-pointer px-2 py-1.5 text-sm',
|
||||
i === highlighted ? 'bg-slate-800 text-white' : 'text-slate-200 hover:bg-slate-800/60'
|
||||
)}
|
||||
onMouseEnter={() => setHighlighted(i)}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
onClick={() => selectRow(row)}
|
||||
>
|
||||
{row.kind === 'clear' ? (
|
||||
<span className="text-slate-400">{clearSelectionLabel}</span>
|
||||
) : (
|
||||
row.value
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+6
-1
@@ -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;
|
||||
|
||||
|
||||
@@ -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.',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Mesh Forge tree URLs: `/owner/repo/tree/<branch segments>/target/<env>`
|
||||
* 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)}`
|
||||
}
|
||||
@@ -106,8 +106,9 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
URLs mirror GitHub: <code className="text-slate-400">/owner/repo/tree/ref</code>. Short form{' '}
|
||||
<code className="text-slate-400">owner/repo</code> uses the default branch.
|
||||
Repo URLs: <code className="text-slate-400">/owner/repo</code> (pick branch),{' '}
|
||||
<code className="text-slate-400">/owner/repo/tree/ref</code>, or add{' '}
|
||||
<code className="text-slate-400">/target/envName</code> to deep-link a PlatformIO environment.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+156
-105
@@ -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<string>('')
|
||||
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<string | null>(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 (
|
||||
<div className="min-h-[40vh] flex items-center justify-center text-slate-400">
|
||||
Resolving default branch…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!branchData.row) {
|
||||
return (
|
||||
<div className="max-w-xl mx-auto px-6 py-16 text-slate-300">
|
||||
<p className="mb-4">Could not load branch list. The repository may be private or missing.</p>
|
||||
<Button asChild variant="outline">
|
||||
<Link to="/">Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const enc = branchData.row.defaultBranch.split('/').map(encodeURIComponent).join('/')
|
||||
return <Navigate to={`/${ownerParam}/${repoParam}/tree/${enc}`} replace />
|
||||
}
|
||||
|
||||
if (!effectiveRef) {
|
||||
if (branchData === undefined) {
|
||||
return (
|
||||
<div className="min-h-[40vh] flex items-center justify-center text-slate-400">Loading…</div>
|
||||
<div className="min-h-[40vh] flex items-center justify-center text-slate-400">Loading repository…</div>
|
||||
)
|
||||
}
|
||||
if (!branchData.row) {
|
||||
return (
|
||||
<div className="max-w-xl mx-auto px-6 py-16 text-slate-300">
|
||||
<p className="mb-4">Could not load branch list. The repository may be private or missing.</p>
|
||||
<Button asChild variant="outline">
|
||||
<Link to="/">Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="max-w-6xl mx-auto px-6 py-10 text-slate-200">
|
||||
<section className="rounded-2xl border border-slate-700/90 bg-slate-950/90 p-6 md:p-8 shadow-xl shadow-black/30">
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-[minmax(0,1fr)_17.5rem] lg:gap-10 items-start">
|
||||
<div className="min-w-0 space-y-5 order-2 lg:order-1">
|
||||
<div className="min-w-0 space-y-5 order-1">
|
||||
<div className="flex flex-nowrap items-end gap-2 overflow-x-auto border-b border-slate-800 pb-3 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<AutocompleteField
|
||||
<ComboboxField
|
||||
label="Branch"
|
||||
layout="inline"
|
||||
id="mesh-forge-branch"
|
||||
options={branchOptions}
|
||||
value={branchDraft}
|
||||
placeholder="--branch--"
|
||||
clearSelectionLabel="Clear branch"
|
||||
onChange={v => {
|
||||
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 ? (
|
||||
<AutocompleteField
|
||||
{hasBranch && scanReady && envNames.length > 0 ? (
|
||||
<ComboboxField
|
||||
label="Target"
|
||||
layout="inline"
|
||||
id="mesh-forge-target"
|
||||
options={envNames}
|
||||
value={envDraft}
|
||||
placeholder="--target--"
|
||||
clearSelectionLabel="Clear target"
|
||||
onChange={v => {
|
||||
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() {
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
disabled
|
||||
disabled={!hasBranch}
|
||||
value=""
|
||||
placeholder={targetPlaceholder}
|
||||
className="h-9 min-w-[7rem] flex-1 cursor-not-allowed rounded-md border border-slate-800 bg-slate-900/50 px-2.5 text-sm text-slate-500 placeholder:text-slate-600"
|
||||
className="h-9 min-w-28 flex-1 cursor-not-allowed rounded-md border border-slate-800 bg-slate-900/50 px-2.5 text-sm text-slate-500 placeholder:text-slate-600"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
@@ -293,14 +310,14 @@ export default function RepoPage() {
|
||||
disabled={flashPrimaryDisabled}
|
||||
onClick={queueFlashArtifacts}
|
||||
>
|
||||
Flash
|
||||
{flashButtonLabel}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-500">
|
||||
{branchData?.isStale ? <span>Branch list may be stale.</span> : null}
|
||||
{refError ? <span className="text-red-400">{refError}</span> : null}
|
||||
{!refError && !resolvedSha ? <span>Resolving branch…</span> : null}
|
||||
{!refError && hasBranch && !resolvedSha ? <span>Resolving branch…</span> : null}
|
||||
{resolvedSha && (scan == null || scan.scanStatus === 'in_progress') ? (
|
||||
<span>Scanning PlatformIO…</span>
|
||||
) : null}
|
||||
@@ -318,16 +335,46 @@ export default function RepoPage() {
|
||||
{build.githubRunId ? (
|
||||
<a
|
||||
className="text-cyan-400 hover:underline text-xs"
|
||||
href={`https://github.com/MeshEnvy/mesh-forge/actions/runs/${build.githubRunId}`}
|
||||
href={`https://github.com/${MESH_FORGE_ACTIONS_REPO}/actions/runs/${build.githubRunId}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Workflow run
|
||||
View run on GitHub
|
||||
</a>
|
||||
) : build.status === 'failed' ? (
|
||||
<a
|
||||
className="text-cyan-400 hover:underline text-xs"
|
||||
href={meshForgeWorkflowUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="No run ID — usually the workflow never started (e.g. dispatch rejected). Open the Mesh Forge workflow to fix YAML or inspect recent runs."
|
||||
>
|
||||
Mesh Forge workflow on GitHub
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
{build.status === 'failed' && build.errorSummary ? (
|
||||
<p className="text-red-300 text-xs whitespace-pre-wrap">{formatBuildErrorSummary(build.errorSummary)}</p>
|
||||
<div className="space-y-2 text-xs">
|
||||
{(() => {
|
||||
const { headline, body } = buildFailurePresentation(build.errorSummary)
|
||||
return (
|
||||
<>
|
||||
<p className="font-medium text-slate-200">{headline}</p>
|
||||
{body ? <p className="text-slate-400 leading-relaxed">{body}</p> : null}
|
||||
<details className="text-slate-500">
|
||||
<summary className="cursor-pointer select-none hover:text-slate-400">
|
||||
Technical details
|
||||
</summary>
|
||||
<pre className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap wrap-break-word text-[11px] text-red-300/90">
|
||||
{build.errorSummary.length > 2500
|
||||
? `${build.errorSummary.slice(0, 2500)}…`
|
||||
: build.errorSummary}
|
||||
</pre>
|
||||
</details>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
) : null}
|
||||
{build.status === 'succeeded' ? (
|
||||
<Button type="button" size="sm" variant="secondary" onClick={() => void download()}>
|
||||
@@ -356,7 +403,9 @@ export default function RepoPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-8 prose prose-invert prose-sm max-w-none prose-hr:my-6">
|
||||
{readmeMd === null ? (
|
||||
{!effectiveRef ? (
|
||||
<p className="text-slate-500 not-prose text-sm">Select a branch to load the README.</p>
|
||||
) : readmeMd === null ? (
|
||||
<p className="text-slate-500 not-prose">Loading…</p>
|
||||
) : (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, rehypeSanitize]}>
|
||||
@@ -366,7 +415,7 @@ export default function RepoPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="order-1 border-b border-slate-800 pb-8 lg:order-2 lg:border-b-0 lg:border-l lg:border-slate-800 lg:pb-0 lg:pl-8 space-y-4">
|
||||
<aside className="order-2 border-b border-slate-800 pb-8 lg:border-b-0 lg:border-l lg:border-slate-800 lg:pb-0 lg:pl-8 space-y-4">
|
||||
<h2 className="text-xs font-semibold text-slate-500 uppercase tracking-wide">About</h2>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">{owner}</p>
|
||||
@@ -380,17 +429,17 @@ export default function RepoPage() {
|
||||
href={ghTree}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title={`View ${effectiveRef} on GitHub`}
|
||||
title={effectiveRef ? `View ${effectiveRef} on GitHub` : 'View repository on GitHub'}
|
||||
>
|
||||
<Github className="size-4" aria-hidden />
|
||||
<span className="sr-only">View {effectiveRef} on GitHub</span>
|
||||
<span className="sr-only">
|
||||
{effectiveRef ? `View ${effectiveRef} on GitHub` : 'View repository on GitHub'}
|
||||
</span>
|
||||
</a>
|
||||
) : null}
|
||||
</h3>
|
||||
</div>
|
||||
{branchData === undefined ? (
|
||||
<p className="text-sm text-slate-500">Loading…</p>
|
||||
) : ghAboutDescription ? (
|
||||
{ghAboutDescription ? (
|
||||
<p className="text-sm text-slate-200 leading-relaxed">{ghAboutDescription}</p>
|
||||
) : null}
|
||||
{ghAboutHomepage ? (
|
||||
@@ -409,10 +458,12 @@ export default function RepoPage() {
|
||||
href={ghTree}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title={`View ${effectiveRef} on GitHub`}
|
||||
title={effectiveRef ? `View ${effectiveRef} on GitHub` : 'View repository on GitHub'}
|
||||
>
|
||||
<Github className="size-4" aria-hidden />
|
||||
<span className="sr-only">View {effectiveRef} on GitHub</span>
|
||||
<span className="sr-only">
|
||||
{effectiveRef ? `View ${effectiveRef} on GitHub` : 'View repository on GitHub'}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
+3
-2
@@ -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, "."),
|
||||
|
||||
Reference in New Issue
Block a user