From ae63378fdb5d55c94e930994bd5dc35d3b76da17 Mon Sep 17 00:00:00 2001 From: "DESKTOP-PUHFO60\\dsx2016" Date: Mon, 19 Oct 2026 19:38:53 +0800 Subject: [PATCH] Add CYD browser flasher with Next.js, i18n, and Web Serial install. Replace the starter app with a public/local flasher that discovers sibling firmwares, supports en/zh, and documents HTTPS deploy. Co-authored-by: Cursor --- .env.example | 5 + .gitignore | 10 +- README.md | 104 +- app/[locale]/layout.tsx | 113 ++ app/[locale]/not-found.tsx | 10 + app/[locale]/page.tsx | 23 + .../[project]/[variant]/[file]/route.ts | 37 + .../[variant]/manifest.json/route.ts | 39 + app/api/firmware/route.ts | 13 + app/globals.css | 98 +- app/layout.tsx | 35 +- app/page.tsx | 103 -- app/robots.ts | 12 + app/sitemap.ts | 17 + components.json | 20 + components/esp-web-install.tsx | 48 + components/flasher-panel.tsx | 162 ++ components/language-switcher.tsx | 38 + components/ui/badge.tsx | 26 + components/ui/button.tsx | 51 + components/ui/card.tsx | 44 + i18n/navigation.ts | 5 + i18n/request.ts | 15 + i18n/routing.ts | 9 + lib/firmware.ts | 246 +++ lib/utils.ts | 12 + messages/en.json | 44 + messages/zh.json | 44 + middleware.ts | 8 + next.config.ts | 9 +- package-lock.json | 1614 ++++++++++++++++- package.json | 27 +- public/file.svg | 1 - public/globe.svg | 1 - public/next.svg | 1 - public/vercel.svg | 1 - public/window.svg | 1 - scripts/sync-firmware.ps1 | 37 + tsconfig.json | 2 +- 39 files changed, 2889 insertions(+), 196 deletions(-) create mode 100644 .env.example create mode 100644 app/[locale]/layout.tsx create mode 100644 app/[locale]/not-found.tsx create mode 100644 app/[locale]/page.tsx create mode 100644 app/api/firmware/[project]/[variant]/[file]/route.ts create mode 100644 app/api/firmware/[project]/[variant]/manifest.json/route.ts create mode 100644 app/api/firmware/route.ts delete mode 100644 app/page.tsx create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts create mode 100644 components.json create mode 100644 components/esp-web-install.tsx create mode 100644 components/flasher-panel.tsx create mode 100644 components/language-switcher.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 i18n/navigation.ts create mode 100644 i18n/request.ts create mode 100644 i18n/routing.ts create mode 100644 lib/firmware.ts create mode 100644 lib/utils.ts create mode 100644 messages/en.json create mode 100644 messages/zh.json create mode 100644 middleware.ts delete mode 100644 public/file.svg delete mode 100644 public/globe.svg delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg delete mode 100644 public/window.svg create mode 100644 scripts/sync-firmware.ps1 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dae4a09 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Public site origin (canonical, Open Graph, sitemap) +NEXT_PUBLIC_SITE_URL=https://your-domain.example + +# Optional: absolute path to firmware parent (sibling projects or a mirror folder) +# CYD_FIRMWARE_ROOT=C:\Users\dsx2016\Desktop\codes\cyd diff --git a/.gitignore b/.gitignore index 5ef6a52..927a854 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,14 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* +# env files +.env +.env.local +.env.*.local +!.env.example + +# mirrored firmwares (run npm run sync:firmware for public deploy) +/firmware/ # vercel .vercel diff --git a/README.md b/README.md index e215bc4..dba01a1 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,98 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# CYD Flasher -## Getting Started +Browser flasher for **Cheap Yellow Display** (ESP32-2432S028). -First, run the development server: +Stack: **Next.js 15** · **Web Serial** · **esp-web-tools** · **next-intl** · **Tailwind / shadcn-style UI** -```bash +| | | +|---|---| +| Locales | English (default) `/en` · 中文 `/zh` | +| SEO | `generateMetadata`, Open Graph, `/sitemap.xml`, `/robots.txt` | +| Flash | `0x1000` bootloader · `0x8000` partitions · `0xe000` boot_app0 · `0x10000` firmware | + +## Quick start (local) + +```powershell +cd c:\Users\dsx2016\Desktop\codes\cyd\cydweb +npm install npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open **Chrome** or **Edge**: [http://localhost:3000](http://localhost:3000) (redirects to `/en`). -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +1. Connect CYD with a data USB cable +2. Select a firmware +3. Click **Connect & flash** and choose the serial port (CH340 / CP210x) -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +Production locally: -## Learn More +```powershell +npm run build +npm start +``` -To learn more about Next.js, take a look at the following resources: +## Firmware discovery -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +By default the app scans sibling projects: -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```text +..//firmware//{bootloader,partitions,boot_app0,firmware}.bin +``` -## Deploy on Vercel +Examples currently picked up: `temperature`, `ld2420test`, `sgp30test`, `phtotest`, `tftest` (Pomodoro). -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +Override search root: -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +```env +CYD_FIRMWARE_ROOT=C:\Users\dsx2016\Desktop\codes\cyd +``` + +After rebuilding a project, refresh its `firmware/` archive (each repo’s `firmware\export_from_build.ps1`), then click **Refresh** on the page. + +## Public deploy + +Web Serial requires **HTTPS** (or localhost). + +1. Mirror firmwares into this repo (no Desktop path dependency): + +```powershell +npm run sync:firmware +# or: .\scripts\sync-firmware.ps1 +``` + +2. Copy `.env.example` → `.env` and set: + +```env +NEXT_PUBLIC_SITE_URL=https://your-domain.example +``` + +3. Deploy with Node (`npm run build && npm start`) or any Next.js host. + +Mirrored binaries live under `firmware/` (gitignored). Run sync in CI before build if needed. + +## Scripts + +| Command | Description | +|---------|-------------| +| `npm run dev` | Dev server (Turbopack) | +| `npm run build` | Production build | +| `npm start` | Serve production build | +| `npm run sync:firmware` | Copy sibling `firmware/*` into `./firmware` | +| `npm run lint` | ESLint | + +## Project layout + +```text +app/[locale]/ # SSR pages (en / zh) +app/api/firmware/ # list · manifest · .bin +components/ # Flasher UI + shadcn-style primitives +messages/ # en.json · zh.json +lib/firmware.ts # discovery + flash offsets +scripts/ # sync-firmware.ps1 +``` + +## 中文说明 + +本机:`npm install` → `npm run dev` → Chrome/Edge 打开 http://localhost:3000 ,右上角可切中文。 + +公网:先 `npm run sync:firmware`,配置 `NEXT_PUBLIC_SITE_URL`(必须 HTTPS),再部署。固件来自各 CYD 项目的 `firmware/` 预编译包,与 PlatformIO / esptool 刷写地址一致。 diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..b9da188 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,113 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { NextIntlClientProvider, hasLocale } from "next-intl"; +import { getMessages, getTranslations, setRequestLocale } from "next-intl/server"; +import { DM_Sans, Syne } from "next/font/google"; +import { routing } from "@/i18n/routing"; +import { LanguageSwitcher } from "@/components/language-switcher"; + +const dmSans = DM_Sans({ + subsets: ["latin"], + variable: "--font-body", + display: "swap", +}); + +const syne = Syne({ + subsets: ["latin"], + variable: "--font-display", + weight: ["600", "700", "800"], + display: "swap", +}); + +type Props = { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}; + +export function generateStaticParams() { + return routing.locales.map((locale) => ({ locale })); +} + +export async function generateMetadata({ params }: Props): Promise { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "meta" }); + const languages = Object.fromEntries( + routing.locales.map((l) => [l, `/${l}`]) + ); + + return { + title: t("title"), + description: t("description"), + metadataBase: new URL( + process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000" + ), + alternates: { + canonical: `/${locale}`, + languages, + }, + openGraph: { + title: t("title"), + description: t("description"), + locale: locale === "zh" ? "zh_CN" : "en_US", + type: "website", + siteName: "CYD Flasher", + }, + twitter: { + card: "summary_large_image", + title: t("title"), + description: t("description"), + }, + robots: { + index: true, + follow: true, + }, + }; +} + +export default async function LocaleLayout({ children, params }: Props) { + const { locale } = await params; + if (!hasLocale(routing.locales, locale)) notFound(); + + setRequestLocale(locale); + const messages = await getMessages(); + const t = await getTranslations("brand"); + const tf = await getTranslations("footer"); + + return ( + + + +
+
+ +
+
+
+ + + {t("name")} + +
+

+ {t("tagline")} +

+
+ +
+ +
+ {children} +
+ +
+ {tf("offsets")} + {tf("stack")} +
+ + + + ); +} diff --git a/app/[locale]/not-found.tsx b/app/[locale]/not-found.tsx new file mode 100644 index 0000000..2ba46f1 --- /dev/null +++ b/app/[locale]/not-found.tsx @@ -0,0 +1,10 @@ +export default function NotFound() { + return ( +
+

+ 404 +

+

Page not found

+
+ ); +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx new file mode 100644 index 0000000..05cbf25 --- /dev/null +++ b/app/[locale]/page.tsx @@ -0,0 +1,23 @@ +import { getTranslations, setRequestLocale } from "next-intl/server"; +import { FlasherPanel } from "@/components/flasher-panel"; + +type Props = { params: Promise<{ locale: string }> }; + +export default async function HomePage({ params }: Props) { + const { locale } = await params; + setRequestLocale(locale); + const t = await getTranslations("hero"); + + return ( +
+
+

+ {t("title")} +

+

{t("lede")}

+
+ + +
+ ); +} diff --git a/app/api/firmware/[project]/[variant]/[file]/route.ts b/app/api/firmware/[project]/[variant]/[file]/route.ts new file mode 100644 index 0000000..92f29f8 --- /dev/null +++ b/app/api/firmware/[project]/[variant]/[file]/route.ts @@ -0,0 +1,37 @@ +import fs from "fs"; +import path from "path"; +import { NextResponse } from "next/server"; +import { REQUIRED_BINS, resolveFirmwareDir } from "@/lib/firmware"; + +export const dynamic = "force-dynamic"; + +type Params = { + params: Promise<{ project: string; variant: string; file: string }>; +}; + +export async function GET(_req: Request, { params }: Params) { + const { project, variant, file } = await params; + if (!(REQUIRED_BINS as readonly string[]).includes(file)) { + return NextResponse.json({ error: "invalid file" }, { status: 400 }); + } + + const dir = resolveFirmwareDir(project, variant); + if (!dir) { + return NextResponse.json({ error: "not found" }, { status: 404 }); + } + + const abs = path.join(dir, file); + if (!fs.existsSync(abs)) { + return NextResponse.json({ error: "not found" }, { status: 404 }); + } + + const buf = fs.readFileSync(abs); + return new NextResponse(buf, { + headers: { + "Content-Type": "application/octet-stream", + "Content-Length": String(buf.length), + "Cache-Control": "no-store", + "Content-Disposition": `attachment; filename="${file}"`, + }, + }); +} diff --git a/app/api/firmware/[project]/[variant]/manifest.json/route.ts b/app/api/firmware/[project]/[variant]/manifest.json/route.ts new file mode 100644 index 0000000..e2aa0e8 --- /dev/null +++ b/app/api/firmware/[project]/[variant]/manifest.json/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { + FLASH_PARTS, + readFirmwareMeta, + resolveFirmwareDir, +} from "@/lib/firmware"; + +export const dynamic = "force-dynamic"; + +type Params = { params: Promise<{ project: string; variant: string }> }; + +export async function GET(_req: Request, { params }: Params) { + const { project, variant } = await params; + const dir = resolveFirmwareDir(project, variant); + if (!dir) { + return NextResponse.json({ error: "firmware not found" }, { status: 404 }); + } + + const meta = readFirmwareMeta(dir); + const name = meta.name || `${project} (${variant})`; + const version = + [meta.date, meta.git].filter(Boolean).join(" / ") || "local"; + + const base = `/api/firmware/${encodeURIComponent(project)}/${encodeURIComponent(variant)}`; + return NextResponse.json({ + name, + version, + new_install_prompt_erase: true, + builds: [ + { + chipFamily: "ESP32", + parts: FLASH_PARTS.map((p) => ({ + path: `${base}/${p.file}`, + offset: p.offset, + })), + }, + ], + }); +} diff --git a/app/api/firmware/route.ts b/app/api/firmware/route.ts new file mode 100644 index 0000000..532607e --- /dev/null +++ b/app/api/firmware/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; +import { discoverFirmware } from "@/lib/firmware"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + const firmwares = discoverFirmware(); + return NextResponse.json({ + chipFamily: "ESP32", + count: firmwares.length, + firmwares, + }); +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..5b90ac1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,96 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --bg: #12100c; + --bg-elev: #1c1812; + --ink: #f4efe4; + --muted: #9a9080; + --line: rgba(244, 239, 228, 0.12); + --accent: #f0c50a; + --accent-hot: #ffd54a; + --accent-ink: #1a1408; + --warn: #e8a45a; + --background: var(--bg); + --foreground: var(--ink); } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + --font-sans: var(--font-body); + --font-display: var(--font-display); } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + background: var(--bg); + color: var(--ink); + font-family: var(--font-body), "PingFang SC", "Microsoft YaHei", "Noto Sans SC", + system-ui, sans-serif; + min-height: 100vh; +} + +.bg-grid { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(240, 197, 10, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(240, 197, 10, 0.04) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(ellipse 80% 60% at 50% 20%, #000 20%, transparent 75%); +} + +.bg-glow { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient(ellipse 50% 40% at 70% -10%, rgba(240, 197, 10, 0.22), transparent 55%), + radial-gradient(ellipse 40% 35% at 10% 80%, rgba(180, 90, 20, 0.12), transparent 50%); +} + +.cta-btn { + appearance: none; + border: none; + font: inherit; + font-family: var(--font-display), system-ui, sans-serif; + font-weight: 700; + letter-spacing: -0.01em; + font-size: 1.05rem; + padding: 0.85rem 1.5rem; + border-radius: 999px; + cursor: pointer; + color: var(--accent-ink); + background: linear-gradient(180deg, var(--accent-hot), var(--accent)); + box-shadow: 0 8px 24px rgba(240, 197, 10, 0.25); + transition: transform 0.15s, filter 0.15s, opacity 0.15s; +} + +.cta-btn:hover:not(:disabled) { + filter: brightness(1.05); + transform: translateY(-1px); +} + +.cta-btn:disabled { + opacity: 0.45; + cursor: not-allowed; + box-shadow: none; + transform: none; +} + +@keyframes rise { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-rise { + animation: rise 0.65s ease-out both; } diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..62d1ef6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,34 +1,7 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import type { ReactNode } from "react"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); +/** Root layout — locale-specific html/lang lives under [locale]. */ +export default function RootLayout({ children }: { children: ReactNode }) { + return children; } diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 21b686d..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- - -
- -
- ); -} diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..ce3da0e --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,12 @@ +import type { MetadataRoute } from "next"; + +export default function robots(): MetadataRoute.Robots { + const base = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000"; + return { + rules: { + userAgent: "*", + allow: "/", + }, + sitemap: `${base}/sitemap.xml`, + }; +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..d607531 --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,17 @@ +import type { MetadataRoute } from "next"; +import { routing } from "@/i18n/routing"; + +export default function sitemap(): MetadataRoute.Sitemap { + const base = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000"; + return routing.locales.map((locale) => ({ + url: `${base}/${locale}`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 1, + alternates: { + languages: Object.fromEntries( + routing.locales.map((l) => [l, `${base}/${l}`]) + ), + }, + })); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..ac9d1fc --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/esp-web-install.tsx b/components/esp-web-install.tsx new file mode 100644 index 0000000..772036a --- /dev/null +++ b/components/esp-web-install.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { createElement, useEffect } from "react"; + +/** + * Loads esp-web-tools once; hosts the install button via slot. + */ +export function EspWebInstall({ + manifest, + label, + disabled, +}: { + manifest: string | null; + label: string; + disabled?: boolean; +}) { + useEffect(() => { + if (document.querySelector('script[data-esp-web-tools="1"]')) return; + + const script = document.createElement("script"); + script.type = "module"; + script.src = + "https://unpkg.com/esp-web-tools@10.1.0/dist/web/install-button.js?module"; + script.dataset.espWebTools = "1"; + document.head.appendChild(script); + }, []); + + const activate = createElement( + "button", + { + slot: "activate", + type: "button", + className: "cta-btn", + disabled: Boolean(disabled || !manifest), + }, + label + ); + + if (!manifest) { + return activate; + } + + return createElement( + "esp-web-install-button", + { manifest }, + activate + ); +} diff --git a/components/flasher-panel.tsx b/components/flasher-panel.tsx new file mode 100644 index 0000000..f915d96 --- /dev/null +++ b/components/flasher-panel.tsx @@ -0,0 +1,162 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import { RefreshCw } from "lucide-react"; +import type { FirmwareInfo } from "@/lib/firmware"; +import { formatBytes, cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardHeader, CardTitle } from "@/components/ui/card"; +import { EspWebInstall } from "@/components/esp-web-install"; + +function supportsWebSerial() { + return typeof navigator !== "undefined" && "serial" in navigator; +} + +export function FlasherPanel() { + const t = useTranslations(); + const [firmwares, setFirmwares] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [serialOk, setSerialOk] = useState(true); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await fetch("/api/firmware", { cache: "no-store" }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = (await res.json()) as { firmwares: FirmwareInfo[] }; + const list = data.firmwares ?? []; + setFirmwares(list); + setSelectedId((prev) => { + if (prev && list.some((f) => f.id === prev)) return prev; + return list[0]?.id ?? null; + }); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + setFirmwares([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + setSerialOk(supportsWebSerial()); + void load(); + }, [load]); + + const selected = firmwares.find((f) => f.id === selectedId) ?? null; + + return ( +
+ {!serialOk && ( + +

{t("browser.unsupported")}

+
+ )} + + + + {t("firmware.title")} + + + +
+ {loading && !firmwares.length && ( +

{t("firmware.scanning")}

+ )} + {error && ( +

+ {t("firmware.error", { message: error })} +

+ )} + {!loading && !error && !firmwares.length && ( +

{t("firmware.empty")}

+ )} + {firmwares.map((fw) => { + const active = fw.id === selectedId; + const meta = [fw.board, fw.date, fw.git ? `git ${fw.git}` : null] + .filter(Boolean) + .join(" · "); + return ( + + ); + })} +
+
+ + +
+ {selected ? ( + <> + {t("firmware.selectedLabel")}{" "} + + {selected.name} + + ({selected.id}) + + ) : ( + {t("firmware.noneSelected")} + )} +
+ + + +
    +
  1. {t("install.steps.1")}
  2. +
  3. {t("install.steps.2")}
  4. +
  5. {t("install.steps.3")}
  6. +
  7. {t("install.steps.4")}
  8. +
+
+
+ ); +} diff --git a/components/language-switcher.tsx b/components/language-switcher.tsx new file mode 100644 index 0000000..c181438 --- /dev/null +++ b/components/language-switcher.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useLocale, useTranslations } from "next-intl"; +import { usePathname, useRouter } from "@/i18n/navigation"; +import { routing, type AppLocale } from "@/i18n/routing"; +import { cn } from "@/lib/utils"; + +export function LanguageSwitcher() { + const t = useTranslations("lang"); + const locale = useLocale() as AppLocale; + const router = useRouter(); + const pathname = usePathname(); + + return ( +
+ {routing.locales.map((loc) => ( + + ))} +
+ ); +} diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..e89c06d --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium", + { + variants: { + variant: { + default: "bg-[rgba(240,197,10,0.12)] text-[var(--accent-hot)]", + muted: "bg-[rgba(0,0,0,0.25)] text-[var(--muted)]", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export function Badge({ + className, + variant, + ...props +}: React.HTMLAttributes & VariantProps) { + return
; +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..7530151 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg)] disabled:pointer-events-none disabled:opacity-45", + { + variants: { + variant: { + default: + "bg-[linear-gradient(180deg,var(--accent-hot),var(--accent))] text-[var(--accent-ink)] shadow-[0_8px_24px_rgba(240,197,10,0.25)] hover:brightness-105", + ghost: + "rounded-lg border border-[var(--line)] bg-transparent text-[var(--muted)] hover:border-[rgba(240,197,10,0.35)] hover:bg-[rgba(240,197,10,0.06)] hover:text-[var(--ink)]", + outline: + "rounded-lg border border-[var(--line)] bg-transparent text-[var(--ink)] hover:border-[rgba(240,197,10,0.35)]", + }, + size: { + default: "h-11 px-6", + sm: "h-8 px-3 text-xs", + lg: "h-12 px-8 text-base", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..453d0f8 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,44 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export function Card({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export function CardHeader({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export function CardTitle({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} diff --git a/i18n/navigation.ts b/i18n/navigation.ts new file mode 100644 index 0000000..2c786c5 --- /dev/null +++ b/i18n/navigation.ts @@ -0,0 +1,5 @@ +import { createNavigation } from "next-intl/navigation"; +import { routing } from "./routing"; + +export const { Link, redirect, usePathname, useRouter, getPathname } = + createNavigation(routing); diff --git a/i18n/request.ts b/i18n/request.ts new file mode 100644 index 0000000..e41644f --- /dev/null +++ b/i18n/request.ts @@ -0,0 +1,15 @@ +import { getRequestConfig } from "next-intl/server"; +import { hasLocale } from "next-intl"; +import { routing } from "./routing"; + +export default getRequestConfig(async ({ requestLocale }) => { + const requested = await requestLocale; + const locale = hasLocale(routing.locales, requested) + ? requested + : routing.defaultLocale; + + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default, + }; +}); diff --git a/i18n/routing.ts b/i18n/routing.ts new file mode 100644 index 0000000..5daf33b --- /dev/null +++ b/i18n/routing.ts @@ -0,0 +1,9 @@ +import { defineRouting } from "next-intl/routing"; + +export const routing = defineRouting({ + locales: ["en", "zh"], + defaultLocale: "en", + localePrefix: "always", +}); + +export type AppLocale = (typeof routing.locales)[number]; diff --git a/lib/firmware.ts b/lib/firmware.ts new file mode 100644 index 0000000..e8729b1 --- /dev/null +++ b/lib/firmware.ts @@ -0,0 +1,246 @@ +import fs from "fs"; +import path from "path"; + +export const REQUIRED_BINS = [ + "bootloader.bin", + "partitions.bin", + "boot_app0.bin", + "firmware.bin", +] as const; + +export const FLASH_PARTS = [ + { file: "bootloader.bin", offset: 0x1000 }, + { file: "partitions.bin", offset: 0x8000 }, + { file: "boot_app0.bin", offset: 0xe000 }, + { file: "firmware.bin", offset: 0x10000 }, +] as const; + +export type FirmwareInfo = { + id: string; + project: string; + variant: string; + name: string; + git: string | null; + date: string | null; + board: string; + flash_size: string; + size: number; + extras: Record; + manifestUrl: string; +}; + +function parseVersionTxt(text: string): Record { + const meta: Record = {}; + for (const raw of text.split(/\r?\n/)) { + const line = raw.trim(); + if (!line || line.startsWith(" ") || line.endsWith(":")) continue; + const idx = line.indexOf(":"); + if (idx <= 0) continue; + const key = line.slice(0, idx).trim(); + const val = line.slice(idx + 1).trim(); + if (key === "offsets" || key === "sha256") continue; + meta[key] = val; + } + return meta; +} + +/** Search roots: env override, bundled ./firmware, sibling projects (..). */ +export function getFirmwareSearchRoots(): string[] { + const roots: string[] = []; + if (process.env.CYD_FIRMWARE_ROOT) { + roots.push(path.resolve(process.env.CYD_FIRMWARE_ROOT)); + } + roots.push(path.resolve(process.cwd(), "firmware")); + roots.push(path.resolve(process.cwd(), "..")); + return [...new Set(roots)]; +} + +function isSafeUnder(root: string, candidate: string): boolean { + const absRoot = path.resolve(root); + const abs = path.resolve(candidate); + return abs === absRoot || abs.startsWith(absRoot + path.sep); +} + +function scanProjectFirmware( + projectRoot: string, + projectName: string, + seen: Set +): FirmwareInfo[] { + const items: FirmwareInfo[] = []; + const fwRoot = path.join(projectRoot, "firmware"); + if (!fs.existsSync(fwRoot)) return items; + + let variants: fs.Dirent[]; + try { + variants = fs.readdirSync(fwRoot, { withFileTypes: true }); + } catch { + return items; + } + + for (const v of variants) { + if (!v.isDirectory()) continue; + const dir = path.join(fwRoot, v.name); + const missing = REQUIRED_BINS.filter((f) => !fs.existsSync(path.join(dir, f))); + if (missing.length) continue; + + const id = `${projectName}/${v.name}`; + if (seen.has(id)) continue; + seen.add(id); + + let meta: Record = {}; + const verPath = path.join(dir, "VERSION.txt"); + if (fs.existsSync(verPath)) { + try { + meta = parseVersionTxt(fs.readFileSync(verPath, "utf8")); + } catch { + /* ignore */ + } + } + + const firmwareStat = fs.statSync(path.join(dir, "firmware.bin")); + const reserved = new Set(["name", "git", "date", "board", "flash_size"]); + items.push({ + id, + project: projectName, + variant: v.name, + name: meta.name || `${projectName} (${v.name})`, + git: meta.git || null, + date: meta.date || null, + board: meta.board || "ESP32-2432S028R", + flash_size: meta.flash_size || "4MB", + size: firmwareStat.size, + extras: Object.fromEntries( + Object.entries(meta).filter(([k]) => !reserved.has(k)) + ), + manifestUrl: `/api/firmware/${encodeURIComponent(projectName)}/${encodeURIComponent(v.name)}/manifest.json`, + }); + } + return items; +} + +/** + * Discover firmwares. + * - Bundled layout: `firmware///*.bin` + * - Sibling layout: `..//firmware//*.bin` + */ +export function discoverFirmware(): FirmwareInfo[] { + const seen = new Set(); + const items: FirmwareInfo[] = []; + + for (const root of getFirmwareSearchRoots()) { + if (!fs.existsSync(root)) continue; + + // Bundled: root itself is the firmware mirror (contains project folders) + // or root is cyd parent with project folders that contain firmware/ + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(root, { withFileTypes: true }); + } catch { + continue; + } + + for (const ent of entries) { + if (!ent.isDirectory()) continue; + if (ent.name === "cydweb" || ent.name === "node_modules" || ent.name.startsWith(".")) { + continue; + } + + const projectPath = path.join(root, ent.name); + + // Sibling project: ../temperature/firmware/... + items.push(...scanProjectFirmware(projectPath, ent.name, seen)); + + // Bundled mirror: firmware/temperature/cyd_28r/... + // Here `root` is ./firmware and `ent` is project name; variants are direct children. + const maybeVariantBins = REQUIRED_BINS.every((f) => + fs.existsSync(path.join(projectPath, f)) + ); + if (maybeVariantBins) { + // Unusual: project folder IS the variant — treat parent name + continue; + } + + // If root is ./firmware, children are projects whose children are variants + const nestedFw = path.join(projectPath, "firmware"); + if (!fs.existsSync(nestedFw)) { + // Direct: firmware// + let variants: fs.Dirent[]; + try { + variants = fs.readdirSync(projectPath, { withFileTypes: true }); + } catch { + continue; + } + for (const v of variants) { + if (!v.isDirectory()) continue; + const dir = path.join(projectPath, v.name); + if (REQUIRED_BINS.some((f) => !fs.existsSync(path.join(dir, f)))) continue; + const id = `${ent.name}/${v.name}`; + if (seen.has(id)) continue; + seen.add(id); + + let meta: Record = {}; + const verPath = path.join(dir, "VERSION.txt"); + if (fs.existsSync(verPath)) { + try { + meta = parseVersionTxt(fs.readFileSync(verPath, "utf8")); + } catch { + /* ignore */ + } + } + const firmwareStat = fs.statSync(path.join(dir, "firmware.bin")); + const reserved = new Set(["name", "git", "date", "board", "flash_size"]); + items.push({ + id, + project: ent.name, + variant: v.name, + name: meta.name || `${ent.name} (${v.name})`, + git: meta.git || null, + date: meta.date || null, + board: meta.board || "ESP32-2432S028R", + flash_size: meta.flash_size || "4MB", + size: firmwareStat.size, + extras: Object.fromEntries( + Object.entries(meta).filter(([k]) => !reserved.has(k)) + ), + manifestUrl: `/api/firmware/${encodeURIComponent(ent.name)}/${encodeURIComponent(v.name)}/manifest.json`, + }); + } + } + } + } + + items.sort((a, b) => a.name.localeCompare(b.name, "en")); + return items; +} + +export function resolveFirmwareDir( + project: string, + variant: string +): string | null { + for (const root of getFirmwareSearchRoots()) { + const candidates = [ + path.join(root, project, "firmware", variant), + path.join(root, project, variant), + ]; + for (const dir of candidates) { + if (!isSafeUnder(root, dir)) continue; + if ( + fs.existsSync(dir) && + REQUIRED_BINS.every((f) => fs.existsSync(path.join(dir, f))) + ) { + return dir; + } + } + } + return null; +} + +export function readFirmwareMeta(dir: string): Record { + const verPath = path.join(dir, "VERSION.txt"); + if (!fs.existsSync(verPath)) return {}; + try { + return parseVersionTxt(fs.readFileSync(verPath, "utf8")); + } catch { + return {}; + } +} diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..26ae837 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,12 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatBytes(n: number): string { + if (n < 1024) return `${n} B`; + if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`; + return `${(n / (1024 * 1024)).toFixed(2)} MB`; +} diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..e02dde6 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,44 @@ +{ + "meta": { + "title": "CYD Flasher — Flash Cheap Yellow Display in the browser", + "description": "Web Serial installer for ESP32-2432S028 (CYD). Pick a firmware and flash over USB from Chrome or Edge." + }, + "brand": { + "name": "CYD Flasher", + "tagline": "Browser serial flash · ESP32-2432S028" + }, + "hero": { + "title": "Pick firmware, plug in USB, flash once", + "lede": "Scans local CYD project firmwares and writes them over Web Serial from your browser." + }, + "browser": { + "unsupported": "Chrome or Edge with Web Serial is required. Open this site on HTTPS or http://localhost." + }, + "firmware": { + "title": "Available firmware", + "refresh": "Refresh", + "scanning": "Scanning…", + "empty": "No firmware found. Ensure complete firmware/*/…bin packages exist, or set CYD_FIRMWARE_ROOT.", + "error": "Scan failed: {message}", + "noneSelected": "No firmware selected", + "selectedLabel": "Selected:" + }, + "install": { + "cta": "Connect & flash", + "steps": { + "1": "Connect CYD with a data USB cable (not charge-only).", + "2": "Select a firmware above.", + "3": "Click Connect & flash, then choose the serial port (CH340 / CP210x).", + "4": "Wait for progress to finish — the board reboots automatically." + } + }, + "lang": { + "label": "Language", + "en": "English", + "zh": "中文" + }, + "footer": { + "offsets": "Flash @ 0x1000 / 0x8000 / 0xe000 / 0x10000", + "stack": "Powered by esp-web-tools · Web Serial flash for ESP32" + } +} diff --git a/messages/zh.json b/messages/zh.json new file mode 100644 index 0000000..716952a --- /dev/null +++ b/messages/zh.json @@ -0,0 +1,44 @@ +{ + "meta": { + "title": "CYD Flasher — 浏览器刷写 Cheap Yellow Display", + "description": "面向 ESP32-2432S028(CYD)的 Web Serial 刷机站。在 Chrome / Edge 中选择固件并通过 USB 刷入。" + }, + "brand": { + "name": "CYD Flasher", + "tagline": "浏览器串口刷机 · ESP32-2432S028" + }, + "hero": { + "title": "选固件,插上 USB,一键刷入", + "lede": "自动扫描本机 CYD 项目预编译包,在浏览器里通过 Web Serial 写入固件。" + }, + "browser": { + "unsupported": "需要支持 Web Serial 的 Chrome 或 Edge。请通过 HTTPS 或 http://localhost 打开本页。" + }, + "firmware": { + "title": "可用固件", + "refresh": "刷新", + "scanning": "正在扫描…", + "empty": "未找到固件。请确认存在完整的 firmware/*/…bin,或设置 CYD_FIRMWARE_ROOT。", + "error": "扫描失败:{message}", + "noneSelected": "尚未选择固件", + "selectedLabel": "已选:" + }, + "install": { + "cta": "连接并刷写", + "steps": { + "1": "用 USB 线连接 CYD(数据口,非仅充电)。", + "2": "选择上方固件。", + "3": "点「连接并刷写」,在弹窗中选串口(常见 CH340 / CP210x)。", + "4": "等待进度完成,板子会自动重启。" + } + }, + "lang": { + "label": "语言", + "en": "English", + "zh": "中文" + }, + "footer": { + "offsets": "刷写地址 @ 0x1000 / 0x8000 / 0xe000 / 0x10000", + "stack": "基于 esp-web-tools · 浏览器串口刷写 ESP32" + } +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..d02ce16 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,8 @@ +import createMiddleware from "next-intl/middleware"; +import { routing } from "./i18n/routing"; + +export default createMiddleware(routing); + +export const config = { + matcher: ["/", "/(en|zh)/:path*", "/((?!api|_next|_vercel|.*\\..*).*)"], +}; diff --git a/next.config.ts b/next.config.ts index e9ffa30..2abcfdf 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,8 @@ import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; -const nextConfig: NextConfig = { - /* config options here */ -}; +const withNextIntl = createNextIntlPlugin("./i18n/request.ts"); -export default nextConfig; +const nextConfig: NextConfig = {}; + +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index e0fc25c..5f31e1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,18 @@ "name": "cydweb", "version": "0.1.0", "dependencies": { + "@radix-ui/react-radio-group": "^1.5.1", + "@radix-ui/react-select": "^2.4.1", + "@radix-ui/react-separator": "^1.2.0", + "@radix-ui/react-slot": "^1.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.59.0", "next": "15.5.28", + "next-intl": "^4.14.9", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "tailwind-merge": "^3.7.0" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -37,6 +46,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eloqnt/config": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@eloqnt/config/-/config-0.1.1.tgz", + "integrity": "sha512-YRDigSaiJEzKDfzTth5kG+L/PKfB3mo8h7yNzpXExPfvQmcD05WoZT57PdhJE0jI+Vva6eb0OCU8hnHHgdME3g==", + "license": "MIT" + }, + "node_modules/@eloqnt/format-json": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eloqnt/format-json/-/format-json-0.1.0.tgz", + "integrity": "sha512-4Pkb+HyzgWNJXoU48aJhUyNZSjZvMGooPCNG1ZFmBm55xTiNNPQCNXH66Ob6sXS6RvbWZrRpCf+Uo/Jz9K9g+A==", + "license": "MIT", + "dependencies": { + "@eloqnt/config": "^0.1.0" + } + }, + "node_modules/@eloqnt/format-po": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@eloqnt/format-po/-/format-po-0.1.1.tgz", + "integrity": "sha512-aLtmIoqXGEaeTT0RcsLN8sZ3aIrsT/c/dVs28zrLhDjO3+vA3KNP+r98N0xq8w/k36+/GIUt4uZNbqcA71GVWw==", + "license": "MIT", + "dependencies": { + "@eloqnt/config": "^0.1.1", + "po-parser": "^2.2.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -214,6 +248,74 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.21.tgz", + "integrity": "sha512-Vnl7IOSyMRaP4Pjv48etkT2nlcIwzh4QWIa3hewzGlZZPBbft+TbMir4MijpDViOLN/1hvkGC10oYMuilsG+fw==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.12" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.12.tgz", + "integrity": "sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA==", + "license": "MIT" + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.14", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.14.tgz", + "integrity": "sha512-8O7V38QmbB11+ryJ3KM/znrzXtiqgpXzqNoqDsndHZZ5/zDKTTL+cf5F0mzXkQEwhx8IlWnqYvEovf2S5EwLTw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1105,6 +1207,903 @@ "node": ">=12.4.0" } }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.2.0.tgz", + "integrity": "sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.17.tgz", + "integrity": "sha512-67jjcVvxjXCCStnTW3Z+M7jmaFMuMR7M2JgpDzH4w7JSRrikmzhjfYgP/09ECb8n7whnsgh1ifJ+Ie96qEj+pQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.2.0.tgz", + "integrity": "sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.3.0.tgz", + "integrity": "sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.2.0.tgz", + "integrity": "sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.2.1.tgz", + "integrity": "sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.2.0.tgz", + "integrity": "sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.3.1.tgz", + "integrity": "sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.4.0.tgz", + "integrity": "sha512-yCWRvLeGv9Pz3+efwHqGPp70mZ56r3kfIgQVF56Oj2aAa4uP/Tb+IE59wNcc+758rctqbCKx4LbK8II/r7Bn0Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.17", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.5", + "@radix-ui/rect": "1.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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.2.0.tgz", + "integrity": "sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.11.tgz", + "integrity": "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.2.0.tgz", + "integrity": "sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.5.1.tgz", + "integrity": "sha512-QbyVT1ddnjqB9H+CUYKkNWdU9fIK3QHCYtjf7mS6ekSqiuXkWtWn5UZW9mhA8qtl3vrVyIsmR9gePstAtKrqkw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-size": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.2.1.tgz", + "integrity": "sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.4.1.tgz", + "integrity": "sha512-888ZwW0Md6GyptuqYABRX9nLKgoYfcooCyBBvVaX5Jp3e+8Jp2i4Zo/Uy50Hm+Ysn2JfloQXstW4U36w8teVtg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.2.0.tgz", + "integrity": "sha512-1jUAap0wZEqVP05Ou7kJgRxe/W+Qftng+rUNWGa0qkKbv9r5pWTG3E7a3ELKo2eJoHKMECnzuYrZpSyO1YgcYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.5.0.tgz", + "integrity": "sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.5.tgz", + "integrity": "sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.3.0.tgz", + "integrity": "sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.5.tgz", + "integrity": "sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.13.tgz", + "integrity": "sha512-feijiN1oPk4rck4u9Dm9tsU4mT6Wp9oeEEhaz1WUEs1VuqLf8xzsFG+Qk+uWq6TnMH9O+WRECRg0NftyP9E4Fw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1119,6 +2118,228 @@ "dev": true, "license": "MIT" }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.18.tgz", + "integrity": "sha512-ozgiCHyFDCwfXzTnaLR+i8WxEyt06GTudn9rPh08vj+u3Z8vMvt5Pi8T+BikyNXm7y++h6UMdqsnJuWrV+tk2A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.18.tgz", + "integrity": "sha512-tuwxXi+sjxxTAi7GP3NeQXQxOf4SXSiFxKAMYLBdoy7JqGYby6uSDXojcFoLOhSp7oiNGstnaFXxfbP0Hpro9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.18.tgz", + "integrity": "sha512-SMdhmY1P9Kveh0MRj6vqQF98bsjnDa0lBBvkd/lcpxmjj5QHJOQAU0N3g6h7GPWpBMs3h2g3lHu/QTDn+uMsXg==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.18.tgz", + "integrity": "sha512-ep/mQyhblQYZ6tkfq0IJspcVSFl5jdywks3kejanrbB31uuujRD/QWOoGNp2L3lLRdAdQvgl/M3pA7g3hSuQug==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.18.tgz", + "integrity": "sha512-pkzGVxrAQu5+febW1a/Aiph3YJk0UiJKXoWbxnDVEIOZWkk5aETHTuyVsOQ3kOgn2dzNMvJVeqcoEQdH+ntqWQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.18.tgz", + "integrity": "sha512-v9kiu4as1pJyQ8ESFTtMNydZ99m86QXLfAtm+XiKPKS93pXL80BCaXokAs9ebioGkIXgmwfbzUCdUC62H3JWkw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.18.tgz", + "integrity": "sha512-vU7TKRJ26i86YQr4QY/5bdcH72KzkBbSSUfPlErW6d6KOR47BcVGlYZFTcMMYlmd3CVSXFHW18tBSila66YfoA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.18.tgz", + "integrity": "sha512-Ml6kbe0BoVJkSgmYPAEUudGcu84jaQeqDN8xeMumbwNpT0vlmm7241Ddp9XKCjQABvjomnXF0OiNOAfjBhNDNw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.18.tgz", + "integrity": "sha512-TGU24+QmmS8y/hg0BLKgOU3W5TCcQC9bgGl8AB0R61gTNQdWdcmCehRLAJwlZfxtAml66xMJ6PWrX8NKrABOsA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.18.tgz", + "integrity": "sha512-S53hm7Amc3YV8t2BUT6R7J+zSnwiGarG+GnxMca4W0uMkxBZ3Gl6jBwEgb6YqrYjf2DzOuRfauztqptCO76a/w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.18.tgz", + "integrity": "sha512-+6/10+OJmU12zWkrPVG0QeQEr8VsWeF8gyXrOfHZMIfMOfz8/ULXVZPhV9GhGMKSWake1xEJj2IYCiFRS/xMpQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.18.tgz", + "integrity": "sha512-7LsVrIICAZWSajXT6DwO7PYSTnW9maamOCyQylaOh13Viptrgi/7hl9fP75SiHe+kscaJahGXcorgt4MOOh/1w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1128,6 +2349,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", @@ -1457,7 +2687,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1467,7 +2697,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.3.0" @@ -2172,6 +3402,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2523,12 +3765,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2556,6 +3819,19 @@ "dev": true, "license": "MIT" }, + "node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2575,7 +3851,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2704,12 +3980,17 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3599,6 +4880,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3801,6 +5091,21 @@ "node": ">= 0.4" } }, + "node_modules/icu-minify": { + "version": "4.14.9", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.14.9.tgz", + "integrity": "sha512-PmO+fNnC3K3mfPKTnm3RrPERrhQZnjm1AlpyZQ0ShbXi8Y/kYZ3YOde8TEjCLz9k3P2DmzkBn8lykGrCAi1Ajw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3853,6 +5158,31 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "11.2.15", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.15.tgz", + "integrity": "sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.18" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.18.tgz", + "integrity": "sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.11" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4018,7 +5348,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4064,7 +5393,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4746,6 +6074,21 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.59.0.tgz", + "integrity": "sha512-5qgMyyCPU6OvnKpLS2t693+ZtiqA8oaSweWoUows0BLFKFf6V75m4+CptpmohZHpD5IuOVIBXHBBiv2vpLJmtw==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4861,6 +6204,22 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/next": { "version": "15.5.28", "resolved": "https://registry.npmjs.org/next/-/next-15.5.28.tgz", @@ -4913,6 +6272,86 @@ } } }, + "node_modules/next-intl": { + "version": "4.14.9", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.14.9.tgz", + "integrity": "sha512-uauRtUC+TrYNA1OL/QcwL9irndLrI9jK3Wdaa9W0B5AeKh9cqVdNeT0IWgLnhBqBRrVwppVdI0M+C1+JdGPQ3w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@eloqnt/config": "^0.1.1", + "@eloqnt/format-json": "^0.1.0", + "@eloqnt/format-po": "^0.1.1", + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "~1.16.0", + "icu-minify": "^4.14.9", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "4.14.9", + "use-intl": "^4.14.9" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.14.9", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.14.9.tgz", + "integrity": "sha512-8jAkmDwQJo6RDavBHm9QRl1YTn2WXf+rI4SVhMaZxZcxopnwZic6fKTXOoNIo3kzC+a6vJySkOkXmHTVFLAYtg==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.16.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.18.tgz", + "integrity": "sha512-0cYQLkpcwUngg55TDwpwrrVOU+B6zh1NFbp+oS7dVFhmYkzaARelwugo/bzPJh1FrcnppmF+KTqioCGwyqbcEw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.28" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.16.18", + "@swc/core-darwin-x64": "1.16.18", + "@swc/core-linux-arm-gnueabihf": "1.16.18", + "@swc/core-linux-arm64-gnu": "1.16.18", + "@swc/core-linux-arm64-musl": "1.16.18", + "@swc/core-linux-ppc64-gnu": "1.16.18", + "@swc/core-linux-s390x-gnu": "1.16.18", + "@swc/core-linux-x64-gnu": "1.16.18", + "@swc/core-linux-x64-musl": "1.16.18", + "@swc/core-win32-arm64-msvc": "1.16.18", + "@swc/core-win32-ia32-msvc": "1.16.18", + "@swc/core-win32-x64-msvc": "1.16.18" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4941,6 +6380,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -5221,6 +6666,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/po-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.2.0.tgz", + "integrity": "sha512-NdTrKgh0oO7+y+RFX8KKhOB438x/j94UGXEFzl/7pHH0JjWSn42iJ9tgmPksIO6n1JKDHmNDcejPJfKspljB9g==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5341,6 +6792,75 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5952,6 +7472,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", @@ -6244,6 +7774,76 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-intl": { + "version": "4.14.9", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.14.9.tgz", + "integrity": "sha512-37ktvdCCz+6JY2VFPl/GV69Ds4ahiUT2c2xEZZRZYxdgGLxFsUbdxzjQVRm3YMIOUU0VPs6zvlifb4Yjk4IxIg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.14.9", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index bc7830b..7acb691 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,38 @@ { "name": "cydweb", - "version": "0.1.0", + "version": "1.0.0", "private": true, + "description": "CYD web flasher — Next.js + esp-web-tools for Cheap Yellow Display", "scripts": { "dev": "next dev --turbopack", - "build": "next build --turbopack", + "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "sync:firmware": "powershell -ExecutionPolicy Bypass -File ./scripts/sync-firmware.ps1" }, "dependencies": { + "@radix-ui/react-radio-group": "^1.5.1", + "@radix-ui/react-select": "^2.4.1", + "@radix-ui/react-separator": "^1.2.0", + "@radix-ui/react-slot": "^1.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.59.0", + "next": "15.5.28", + "next-intl": "^4.14.9", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.5.28" + "tailwind-merge": "^3.7.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.5.28", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/scripts/sync-firmware.ps1 b/scripts/sync-firmware.ps1 new file mode 100644 index 0000000..c1615a1 --- /dev/null +++ b/scripts/sync-firmware.ps1 @@ -0,0 +1,37 @@ +# Mirror sibling project firmware archives into cydweb/firmware for public deploy. +# Usage (from cydweb): +# .\scripts\sync-firmware.ps1 + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$cydRoot = Split-Path -Parent $root +$dstRoot = Join-Path $root "firmware" + +New-Item -ItemType Directory -Force -Path $dstRoot | Out-Null + +$required = @("bootloader.bin", "partitions.bin", "boot_app0.bin", "firmware.bin") +$count = 0 + +Get-ChildItem $cydRoot -Directory | Where-Object { $_.Name -ne "cydweb" } | ForEach-Object { + $proj = $_.Name + $fwRoot = Join-Path $_.FullName "firmware" + if (-not (Test-Path $fwRoot)) { return } + + Get-ChildItem $fwRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object { + $variant = $_.Name + $src = $_.FullName + $ok = $true + foreach ($f in $required) { + if (-not (Test-Path (Join-Path $src $f))) { $ok = $false; break } + } + if (-not $ok) { return } + + $dst = Join-Path $dstRoot (Join-Path $proj $variant) + New-Item -ItemType Directory -Force -Path $dst | Out-Null + Copy-Item (Join-Path $src "*") $dst -Force -Recurse + Write-Host "Synced $proj/$variant" + $script:count++ + } +} + +Write-Host "Done. $count firmware package(s) -> $dstRoot" diff --git a/tsconfig.json b/tsconfig.json index d8b9323..90b988a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,6 @@ "@/*": ["./*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "types/**/*.d.ts"], "exclude": ["node_modules"] }