From 15cb870bafab2a032d9aef4f4be2bc7d328b2b9c Mon Sep 17 00:00:00 2001 From: Jeff Lau Date: Mon, 14 Sep 2026 16:47:33 +0000 Subject: [PATCH 1/9] feat(site): scaffold @enspack/site catalog app (WP-19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite + React + TS static app under apps/site reading the indexer JSON API (MVP.md §4.3). Adds the typed data layer (HttpIndexClient / FixtureIndexClient), config from VITE_* env only, shared primitives (Hash, CopyButton, Badge, states, Layout), design tokens, recorded fixtures and route stubs for the pages. Co-authored-by: Jeff Lau --- apps/site/index.html | 17 + apps/site/package.json | 35 + apps/site/public/_redirects | 1 + apps/site/src/app.tsx | 23 + apps/site/src/components/badge.tsx | 46 + apps/site/src/components/copy-button.tsx | 32 + apps/site/src/components/external-link.tsx | 17 + apps/site/src/components/hash.tsx | 35 + apps/site/src/components/layout.tsx | 40 + apps/site/src/components/states.tsx | 34 + apps/site/src/demo/fixtures.ts | 26 + apps/site/src/lib/api.ts | 75 + apps/site/src/lib/client-context.tsx | 29 + apps/site/src/lib/client.ts | 150 ++ apps/site/src/lib/config.ts | 29 + apps/site/src/lib/format.ts | 63 + apps/site/src/lib/use-query.ts | 40 + apps/site/src/main.tsx | 20 + apps/site/src/pages/get-started.tsx | 3 + apps/site/src/pages/home.tsx | 3 + apps/site/src/pages/model.tsx | 3 + apps/site/src/pages/not-found.tsx | 3 + apps/site/src/pages/publisher.tsx | 3 + apps/site/src/pages/violations.tsx | 3 + apps/site/src/styles/global.css | 231 +++ apps/site/src/styles/tokens.css | 63 + apps/site/src/vite-env.d.ts | 8 + apps/site/test/fixtures/attestation-jeff.json | 17 + apps/site/test/fixtures/name-jeff.json | 86 ++ apps/site/test/fixtures/name-llama.json | 97 ++ apps/site/test/fixtures/name-qwen.json | 140 ++ apps/site/test/fixtures/names.json | 41 + apps/site/test/fixtures/publishers.json | 14 + apps/site/test/fixtures/violations-one.json | 11 + apps/site/test/fixtures/violations.json | 3 + apps/site/test/helpers.tsx | 30 + apps/site/test/lib.test.ts | 93 ++ apps/site/test/setup.ts | 1 + apps/site/tsconfig.json | 13 + apps/site/vite.config.ts | 7 + apps/site/vitest.config.ts | 11 + pnpm-lock.yaml | 1342 ++++++++++++++--- pnpm-workspace.yaml | 1 + 43 files changed, 2726 insertions(+), 213 deletions(-) create mode 100644 apps/site/index.html create mode 100644 apps/site/package.json create mode 100644 apps/site/public/_redirects create mode 100644 apps/site/src/app.tsx create mode 100644 apps/site/src/components/badge.tsx create mode 100644 apps/site/src/components/copy-button.tsx create mode 100644 apps/site/src/components/external-link.tsx create mode 100644 apps/site/src/components/hash.tsx create mode 100644 apps/site/src/components/layout.tsx create mode 100644 apps/site/src/components/states.tsx create mode 100644 apps/site/src/demo/fixtures.ts create mode 100644 apps/site/src/lib/api.ts create mode 100644 apps/site/src/lib/client-context.tsx create mode 100644 apps/site/src/lib/client.ts create mode 100644 apps/site/src/lib/config.ts create mode 100644 apps/site/src/lib/format.ts create mode 100644 apps/site/src/lib/use-query.ts create mode 100644 apps/site/src/main.tsx create mode 100644 apps/site/src/pages/get-started.tsx create mode 100644 apps/site/src/pages/home.tsx create mode 100644 apps/site/src/pages/model.tsx create mode 100644 apps/site/src/pages/not-found.tsx create mode 100644 apps/site/src/pages/publisher.tsx create mode 100644 apps/site/src/pages/violations.tsx create mode 100644 apps/site/src/styles/global.css create mode 100644 apps/site/src/styles/tokens.css create mode 100644 apps/site/src/vite-env.d.ts create mode 100644 apps/site/test/fixtures/attestation-jeff.json create mode 100644 apps/site/test/fixtures/name-jeff.json create mode 100644 apps/site/test/fixtures/name-llama.json create mode 100644 apps/site/test/fixtures/name-qwen.json create mode 100644 apps/site/test/fixtures/names.json create mode 100644 apps/site/test/fixtures/publishers.json create mode 100644 apps/site/test/fixtures/violations-one.json create mode 100644 apps/site/test/fixtures/violations.json create mode 100644 apps/site/test/helpers.tsx create mode 100644 apps/site/test/lib.test.ts create mode 100644 apps/site/test/setup.ts create mode 100644 apps/site/tsconfig.json create mode 100644 apps/site/vite.config.ts create mode 100644 apps/site/vitest.config.ts diff --git a/apps/site/index.html b/apps/site/index.html new file mode 100644 index 0000000..5e1b9b4 --- /dev/null +++ b/apps/site/index.html @@ -0,0 +1,17 @@ + + + + + + + + enspack + + +
+ + + diff --git a/apps/site/package.json b/apps/site/package.json new file mode 100644 index 0000000..d265ad8 --- /dev/null +++ b/apps/site/package.json @@ -0,0 +1,35 @@ +{ + "name": "@enspack/site", + "version": "0.1.0", + "private": true, + "description": "enspack catalog site: read-only window onto the indexer JSON API (MVP.md §4.3)", + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit -p tsconfig.json", + "test": "vitest run", + "check": "pnpm typecheck && pnpm test" + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.6.0" + }, + "devDependencies": { + "@enspack/core": "workspace:*", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.4.0", + "jsdom": "^26.1.0", + "typescript": "^5.7.0", + "vite": "^6.3.0", + "vitest": "^3.0.0" + } +} diff --git a/apps/site/public/_redirects b/apps/site/public/_redirects new file mode 100644 index 0000000..bbb3e7a --- /dev/null +++ b/apps/site/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/apps/site/src/app.tsx b/apps/site/src/app.tsx new file mode 100644 index 0000000..3377c46 --- /dev/null +++ b/apps/site/src/app.tsx @@ -0,0 +1,23 @@ +import { Route, Routes } from "react-router"; +import { Layout } from "./components/layout.js"; +import { GetStartedPage } from "./pages/get-started.js"; +import { HomePage } from "./pages/home.js"; +import { ModelPage } from "./pages/model.js"; +import { NotFoundPage } from "./pages/not-found.js"; +import { PublisherPage } from "./pages/publisher.js"; +import { ViolationsPage } from "./pages/violations.js"; + +export function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/apps/site/src/components/badge.tsx b/apps/site/src/components/badge.tsx new file mode 100644 index 0000000..82f813e --- /dev/null +++ b/apps/site/src/components/badge.tsx @@ -0,0 +1,46 @@ +import type { ReactNode } from "react"; + +export type BadgeTone = "neutral" | "accent" | "ok" | "warn" | "danger"; + +export function Badge({ + children, + tone = "neutral", + title, +}: { + children: ReactNode; + tone?: BadgeTone; + title?: string; +}) { + return ( + + {children} + + ); +} + +const ROLE_TONE: Record = { + weight: "accent", + config: "neutral", + tokenizer: "neutral", + index: "neutral", + doc: "neutral", + license: "warn", + code: "neutral", + other: "neutral", +}; + +export function RoleBadge({ role }: { role: string | undefined }) { + const r = role ?? "other"; + return {r}; +} + +export function LicenseBadge({ license, href }: { license: string; href?: string }) { + const badge = {license}; + return href ? ( + + {badge} + + ) : ( + badge + ); +} diff --git a/apps/site/src/components/copy-button.tsx b/apps/site/src/components/copy-button.tsx new file mode 100644 index 0000000..175eb55 --- /dev/null +++ b/apps/site/src/components/copy-button.tsx @@ -0,0 +1,32 @@ +import { useEffect, useState } from "react"; + +export function CopyButton({ value, label = "Copy" }: { value: string; label?: string }) { + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (!copied) return; + const t = setTimeout(() => setCopied(false), 1500); + return () => clearTimeout(t); + }, [copied]); + + async function onClick() { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + } catch { + // clipboard unavailable (insecure context); leave the value selectable instead + } + } + + return ( + + ); +} diff --git a/apps/site/src/components/external-link.tsx b/apps/site/src/components/external-link.tsx new file mode 100644 index 0000000..83a6996 --- /dev/null +++ b/apps/site/src/components/external-link.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react"; + +export function ExternalLink({ + href, + children, + className, +}: { + href: string; + children: ReactNode; + className?: string; +}) { + return ( + + {children} + + ); +} diff --git a/apps/site/src/components/hash.tsx b/apps/site/src/components/hash.tsx new file mode 100644 index 0000000..c3d2734 --- /dev/null +++ b/apps/site/src/components/hash.tsx @@ -0,0 +1,35 @@ +import { shortHex } from "../lib/format.js"; +import { CopyButton } from "./copy-button.js"; + +/** + * A hash, CID, name or magnet in monospace with a copy control. Full value by default; `short` + * shows an abbreviation but keeps the full value in `title` and on the clipboard. + */ +export function Hash({ + value, + short = false, + href, + className, +}: { + value: string; + short?: boolean; + href?: string; + className?: string; +}) { + const text = short ? shortHex(value, 10, 6) : value; + const inner = href ? ( + + {text} + + ) : ( + + {text} + + ); + return ( + + {inner} + + + ); +} diff --git a/apps/site/src/components/layout.tsx b/apps/site/src/components/layout.tsx new file mode 100644 index 0000000..20b78fd --- /dev/null +++ b/apps/site/src/components/layout.tsx @@ -0,0 +1,40 @@ +import type { ReactNode } from "react"; +import { Link, NavLink } from "react-router"; +import { config } from "../lib/config.js"; + +export function Layout({ children }: { children: ReactNode }) { + return ( +
+ {config.chain !== "mainnet" && ( +
+ Showing {config.chain} testnet data. Mainnet follows once the v1 path is live. +
+ )} +
+ + enspack + + +
+
{children}
+ +
+ ); +} diff --git a/apps/site/src/components/states.tsx b/apps/site/src/components/states.tsx new file mode 100644 index 0000000..807f493 --- /dev/null +++ b/apps/site/src/components/states.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from "react"; +import { ApiError } from "../lib/api.js"; + +export function Loading({ label = "Loading" }: { label?: string }) { + return ( + + {label}… + + ); +} + +export function EmptyState({ title, children }: { title: string; children?: ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +export function ErrorState({ error, what = "data" }: { error: Error; what?: string }) { + const detail = + error instanceof ApiError + ? error.status === 0 + ? "The indexer could not be reached." + : `The indexer answered ${error.status} (${error.code}).` + : error.message; + return ( +
+

Could not load {what}

+

{detail}

+
+ ); +} diff --git a/apps/site/src/demo/fixtures.ts b/apps/site/src/demo/fixtures.ts new file mode 100644 index 0000000..e998dce --- /dev/null +++ b/apps/site/src/demo/fixtures.ts @@ -0,0 +1,26 @@ +import attestationJeff from "../../test/fixtures/attestation-jeff.json"; +import nameJeff from "../../test/fixtures/name-jeff.json"; +import nameLlama from "../../test/fixtures/name-llama.json"; +import nameQwen from "../../test/fixtures/name-qwen.json"; +import names from "../../test/fixtures/names.json"; +import publishers from "../../test/fixtures/publishers.json"; +import violations from "../../test/fixtures/violations.json"; +import type { + Attestation, + NameDetail, + NamesPage, + PublishersPage, + ViolationsPage, +} from "../lib/api.js"; +import type { FixtureData } from "../lib/client.js"; + +const details = [nameQwen, nameLlama, nameJeff] as unknown as NameDetail[]; + +/** Recorded indexer responses. Shared by unit tests and `VITE_DATA_SOURCE=demo`. */ +export const demoFixtures: FixtureData = { + names: names as NamesPage, + details: Object.fromEntries(details.map((d) => [d.model, d])), + publishers: publishers as PublishersPage, + violations: violations as ViolationsPage, + attestations: { jeff: attestationJeff as Attestation }, +}; diff --git a/apps/site/src/lib/api.ts b/apps/site/src/lib/api.ts new file mode 100644 index 0000000..a7eedb4 --- /dev/null +++ b/apps/site/src/lib/api.ts @@ -0,0 +1,75 @@ +import type { Manifest, ManifestFile } from "@enspack/core"; + +export type { Manifest, ManifestFile }; + +/** Mirrors `services/indexer/src/api/app.ts` (MVP.md §4.3). Do not widen without changing the indexer. */ +export interface NameListItem { + model: string; + publisher: string; + latest: { name: string; version: string; cid: string }; + upstream: string; + license: string; + totalSize: number; +} + +export interface NamesPage { + items: NameListItem[]; + nextCursor: string | null; +} + +export interface NameVersion { + name: string; + version: string; + cid: string; + createdAt: string; +} + +export interface NameDetail { + model: string; + versions: NameVersion[]; + manifest: Manifest; +} + +export interface PublisherItem { + name: string; + hf: string | null; + models: number; +} + +export interface PublishersPage { + items: PublisherItem[]; +} + +export interface Violation { + name: string; + node: string; + previousCid: string; + newCid: string; + block: number; +} + +export interface ViolationsPage { + items: Violation[]; +} + +/** Registrar `GET /v1/publishers/:label`. Shape is the registrar's; render as key/value. */ +export type Attestation = Record; + +export interface ListNamesParams { + q?: string; + publisher?: string; + cursor?: string; + limit?: number; +} + +export class ApiError extends Error { + readonly status: number; + readonly code: string; + + constructor(status: number, code: string, message: string) { + super(message); + this.name = "ApiError"; + this.status = status; + this.code = code; + } +} diff --git a/apps/site/src/lib/client-context.tsx b/apps/site/src/lib/client-context.tsx new file mode 100644 index 0000000..65daa49 --- /dev/null +++ b/apps/site/src/lib/client-context.tsx @@ -0,0 +1,29 @@ +import { type ReactNode, createContext, useContext } from "react"; +import { demoFixtures } from "../demo/fixtures.js"; +import { FixtureIndexClient, HttpIndexClient, type IndexClient } from "./client.js"; +import { type SiteConfig, config } from "./config.js"; + +export function createClient(cfg: SiteConfig): IndexClient { + return cfg.dataSource === "demo" + ? new FixtureIndexClient(demoFixtures) + : new HttpIndexClient(cfg.indexUrl, cfg.registrarUrl); +} + +const ClientContext = createContext(null); + +export function ClientProvider({ + client, + children, +}: { + client?: IndexClient; + children: ReactNode; +}) { + const value = client ?? createClient(config); + return {children}; +} + +export function useClient(): IndexClient { + const c = useContext(ClientContext); + if (c === null) throw new Error("useClient outside ClientProvider"); + return c; +} diff --git a/apps/site/src/lib/client.ts b/apps/site/src/lib/client.ts new file mode 100644 index 0000000..f842816 --- /dev/null +++ b/apps/site/src/lib/client.ts @@ -0,0 +1,150 @@ +import { + ApiError, + type Attestation, + type ListNamesParams, + type NameDetail, + type NamesPage, + type PublishersPage, + type ViolationsPage, +} from "./api.js"; + +export interface IndexClient { + listNames(params?: ListNamesParams): Promise; + getName(name: string): Promise; + listPublishers(): Promise; + listViolations(): Promise; + /** Resolves to null on 404 (name not issued by the registrar). */ + getAttestation(label: string): Promise; +} + +async function getJson(url: string, fetchImpl: typeof fetch): Promise { + let res: Response; + try { + res = await fetchImpl(url, { headers: { accept: "application/json" } }); + } catch (err) { + throw new ApiError(0, "NETWORK", err instanceof Error ? err.message : "network error"); + } + if (!res.ok) { + let code = "HTTP"; + let message = `${res.status} ${res.statusText}`; + try { + const body = (await res.json()) as { error?: string; code?: string }; + if (typeof body.code === "string") code = body.code; + if (typeof body.error === "string") message = body.error; + } catch { + // non-JSON error body: keep the status text + } + throw new ApiError(res.status, code, message); + } + return (await res.json()) as T; +} + +export class HttpIndexClient implements IndexClient { + private readonly indexUrl: string; + private readonly registrarUrl: string; + private readonly fetchImpl: typeof fetch; + + constructor(indexUrl: string, registrarUrl: string, fetchImpl: typeof fetch = fetch) { + this.indexUrl = indexUrl; + this.registrarUrl = registrarUrl; + this.fetchImpl = fetchImpl; + } + + listNames(params: ListNamesParams = {}): Promise { + const qs = new URLSearchParams(); + if (params.q) qs.set("q", params.q); + if (params.publisher) qs.set("publisher", params.publisher); + if (params.cursor) qs.set("cursor", params.cursor); + if (params.limit !== undefined) qs.set("limit", String(params.limit)); + const suffix = qs.size > 0 ? `?${qs.toString()}` : ""; + return getJson(`${this.indexUrl}/v1/names${suffix}`, this.fetchImpl); + } + + getName(name: string): Promise { + return getJson( + `${this.indexUrl}/v1/names/${encodeURIComponent(name)}`, + this.fetchImpl, + ); + } + + listPublishers(): Promise { + return getJson(`${this.indexUrl}/v1/publishers`, this.fetchImpl); + } + + listViolations(): Promise { + return getJson(`${this.indexUrl}/v1/violations`, this.fetchImpl); + } + + async getAttestation(label: string): Promise { + try { + return await getJson( + `${this.registrarUrl}/v1/publishers/${encodeURIComponent(label)}`, + this.fetchImpl, + ); + } catch (err) { + if (err instanceof ApiError && err.status === 404) return null; + throw err; + } + } +} + +export interface FixtureData { + names: NamesPage; + details: Record; + publishers: PublishersPage; + violations: ViolationsPage; + attestations: Record; +} + +/** Serves recorded fixtures. Used by tests and by `VITE_DATA_SOURCE=demo`. */ +export class FixtureIndexClient implements IndexClient { + private readonly data: FixtureData; + + constructor(data: FixtureData) { + this.data = data; + } + + async listNames(params: ListNamesParams = {}): Promise { + let items = this.data.names.items; + if (params.publisher) items = items.filter((i) => i.publisher === params.publisher); + if (params.q) { + const needle = params.q.toLowerCase(); + items = items.filter((i) => { + const display = this.data.details[i.model]?.manifest.displayName ?? ""; + return ( + i.model.toLowerCase().includes(needle) || + i.upstream.toLowerCase().includes(needle) || + display.toLowerCase().includes(needle) + ); + }); + } + if (params.cursor) items = items.filter((i) => i.model > (params.cursor as string)); + const limit = params.limit ?? 50; + const page = items.slice(0, limit); + const last = page[page.length - 1]; + const nextCursor = + page.length === limit && items.length > page.length && last ? last.model : null; + return { items: page, nextCursor }; + } + + async getName(name: string): Promise { + const direct = this.data.details[name]; + if (direct) return direct; + for (const d of Object.values(this.data.details)) { + if (d.versions.some((v) => v.name === name)) return d; + } + throw new ApiError(404, "NOT_FOUND", "unknown name"); + } + + async listPublishers(): Promise { + return this.data.publishers; + } + + async listViolations(): Promise { + return this.data.violations; + } + + async getAttestation(label: string): Promise { + return this.data.attestations[label] ?? null; + } +} diff --git a/apps/site/src/lib/config.ts b/apps/site/src/lib/config.ts new file mode 100644 index 0000000..78b0a42 --- /dev/null +++ b/apps/site/src/lib/config.ts @@ -0,0 +1,29 @@ +export type Chain = "mainnet" | "sepolia"; +export type DataSource = "http" | "demo"; + +export interface SiteConfig { + indexUrl: string; + registrarUrl: string; + chain: Chain; + dataSource: DataSource; + repoUrl: string; +} + +function stripSlash(u: string): string { + return u.endsWith("/") ? u.slice(0, -1) : u; +} + +/** Reads Vite env once. Only public URLs and the chain name; never keys or RPC URLs. */ +export function readConfig(env: ImportMetaEnv = import.meta.env): SiteConfig { + const chain: Chain = env.VITE_CHAIN === "mainnet" ? "mainnet" : "sepolia"; + const dataSource: DataSource = env.VITE_DATA_SOURCE === "demo" ? "demo" : "http"; + return { + indexUrl: stripSlash(env.VITE_INDEX_URL ?? "https://index.enspack.dev"), + registrarUrl: stripSlash(env.VITE_REGISTRAR_URL ?? "https://registrar.enspack.dev"), + chain, + dataSource, + repoUrl: "https://github.com/jefflau/enspack", + }; +} + +export const config: SiteConfig = readConfig(); diff --git a/apps/site/src/lib/format.ts b/apps/site/src/lib/format.ts new file mode 100644 index 0000000..fc32444 --- /dev/null +++ b/apps/site/src/lib/format.ts @@ -0,0 +1,63 @@ +import type { Chain } from "./config.js"; + +const UNITS = ["B", "KB", "MB", "GB", "TB"] as const; + +export function formatBytes(n: number): string { + if (!Number.isFinite(n) || n < 0) return "–"; + let v = n; + let i = 0; + while (v >= 1000 && i < UNITS.length - 1) { + v /= 1000; + i += 1; + } + const digits = i === 0 ? 0 : v >= 100 ? 0 : v >= 10 ? 1 : 2; + return `${v.toFixed(digits)} ${UNITS[i]}`; +} + +export function formatDate(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + return d.toISOString().slice(0, 10); +} + +/** `0xabcd…1234` style; keeps the full value available via `title` at the call site. */ +export function shortHex(hex: string, head = 6, tail = 4): string { + if (hex.length <= head + tail + 1) return hex; + return `${hex.slice(0, head)}…${hex.slice(-tail)}`; +} + +/** First label of an ENS name: `jeff.enspack.eth` → `jeff`. */ +export function firstLabel(name: string): string { + return name.split(".")[0] ?? name; +} + +export function explorerTx(chain: Chain, hash: string): string { + const host = chain === "mainnet" ? "etherscan.io" : "sepolia.etherscan.io"; + return `https://${host}/tx/${hash}`; +} + +export function explorerBlock(chain: Chain, block: number): string { + const host = chain === "mainnet" ? "etherscan.io" : "sepolia.etherscan.io"; + return `https://${host}/block/${block}`; +} + +export function ensAppUrl(chain: Chain, name: string): string { + const host = chain === "mainnet" ? "app.ens.domains" : "sepolia.app.ens.domains"; + return `https://${host}/${name}`; +} + +export function ipfsUrl(cid: string): string { + return `https://${cid}.ipfs.dweb.link/`; +} + +export function hfUrl(namespaceOrRepo: string): string { + return `https://huggingface.co/${namespaceOrRepo}`; +} + +export function isTxHash(v: unknown): v is string { + return typeof v === "string" && /^0x[0-9a-fA-F]{64}$/.test(v); +} + +export function isAddress(v: unknown): v is string { + return typeof v === "string" && /^0x[0-9a-fA-F]{40}$/.test(v); +} diff --git a/apps/site/src/lib/use-query.ts b/apps/site/src/lib/use-query.ts new file mode 100644 index 0000000..c50d42c --- /dev/null +++ b/apps/site/src/lib/use-query.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; + +export interface QueryState { + data: T | null; + error: Error | null; + loading: boolean; +} + +/** + * Minimal async hook. `key` must change when the inputs change; the effect re-runs on it and + * ignores results from superseded runs. + */ +export function useQuery(key: string, run: () => Promise): QueryState { + const [state, setState] = useState>({ data: null, error: null, loading: true }); + + // biome-ignore lint/correctness/useExhaustiveDependencies: `run` is keyed by `key` on purpose + useEffect(() => { + let cancelled = false; + setState({ data: null, error: null, loading: true }); + run().then( + (data) => { + if (!cancelled) setState({ data, error: null, loading: false }); + }, + (err: unknown) => { + if (!cancelled) { + setState({ + data: null, + error: err instanceof Error ? err : new Error(String(err)), + loading: false, + }); + } + }, + ); + return () => { + cancelled = true; + }; + }, [key]); + + return state; +} diff --git a/apps/site/src/main.tsx b/apps/site/src/main.tsx new file mode 100644 index 0000000..4d2469f --- /dev/null +++ b/apps/site/src/main.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router"; +import { App } from "./app.js"; +import { ClientProvider } from "./lib/client-context.js"; +import "./styles/tokens.css"; +import "./styles/global.css"; + +const root = document.getElementById("root"); +if (root === null) throw new Error("missing #root"); + +createRoot(root).render( + + + + + + + , +); diff --git a/apps/site/src/pages/get-started.tsx b/apps/site/src/pages/get-started.tsx new file mode 100644 index 0000000..15e4364 --- /dev/null +++ b/apps/site/src/pages/get-started.tsx @@ -0,0 +1,3 @@ +export function GetStartedPage() { + return
GetStartedPage
; +} diff --git a/apps/site/src/pages/home.tsx b/apps/site/src/pages/home.tsx new file mode 100644 index 0000000..068d0bc --- /dev/null +++ b/apps/site/src/pages/home.tsx @@ -0,0 +1,3 @@ +export function HomePage() { + return
HomePage
; +} diff --git a/apps/site/src/pages/model.tsx b/apps/site/src/pages/model.tsx new file mode 100644 index 0000000..bcce01f --- /dev/null +++ b/apps/site/src/pages/model.tsx @@ -0,0 +1,3 @@ +export function ModelPage() { + return
ModelPage
; +} diff --git a/apps/site/src/pages/not-found.tsx b/apps/site/src/pages/not-found.tsx new file mode 100644 index 0000000..4bfd14d --- /dev/null +++ b/apps/site/src/pages/not-found.tsx @@ -0,0 +1,3 @@ +export function NotFoundPage() { + return
NotFoundPage
; +} diff --git a/apps/site/src/pages/publisher.tsx b/apps/site/src/pages/publisher.tsx new file mode 100644 index 0000000..b00b8fa --- /dev/null +++ b/apps/site/src/pages/publisher.tsx @@ -0,0 +1,3 @@ +export function PublisherPage() { + return
PublisherPage
; +} diff --git a/apps/site/src/pages/violations.tsx b/apps/site/src/pages/violations.tsx new file mode 100644 index 0000000..7e24643 --- /dev/null +++ b/apps/site/src/pages/violations.tsx @@ -0,0 +1,3 @@ +export function ViolationsPage() { + return
ViolationsPage
; +} diff --git a/apps/site/src/styles/global.css b/apps/site/src/styles/global.css new file mode 100644 index 0000000..badedfe --- /dev/null +++ b/apps/site/src/styles/global.css @@ -0,0 +1,231 @@ +/* Base reset and shared primitives. Page-specific rules live in src/styles/pages/*.css. */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-sans); + font-size: var(--fs-md); + line-height: 1.5; +} + +code, +pre, +.mono { + font-family: var(--font-mono); + font-size: 0.92em; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +button { + font: inherit; + color: inherit; +} + +.muted { + color: var(--muted); +} + +/* Layout */ +.site { + min-height: 100dvh; + display: flex; + flex-direction: column; +} + +.banner { + background: var(--warn-soft); + color: var(--text); + border-bottom: 1px solid var(--border); + text-align: center; + font-size: var(--fs-sm); + padding: var(--sp-2) var(--sp-4); +} + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sp-6); + max-width: var(--content-max); + width: 100%; + margin: 0 auto; + padding: var(--sp-4) var(--sp-6); +} + +.wordmark { + font-family: var(--font-mono); + font-weight: 600; + font-size: var(--fs-lg); + color: var(--text); +} + +.site-nav { + display: flex; + gap: var(--sp-5); + font-size: var(--fs-sm); +} + +.site-nav a { + color: var(--muted); +} + +.site-nav a.active, +.site-nav a:hover { + color: var(--text); + text-decoration: none; +} + +.site-main { + flex: 1; + width: 100%; + max-width: var(--content-max); + margin: 0 auto; + padding: var(--sp-6) var(--sp-6) var(--sp-16); +} + +.site-footer { + border-top: 1px solid var(--border); + color: var(--muted); + font-size: var(--fs-xs); + display: flex; + justify-content: space-between; + gap: var(--sp-4); + max-width: var(--content-max); + width: 100%; + margin: 0 auto; + padding: var(--sp-4) var(--sp-6); +} + +/* Primitives */ +.copy-btn { + border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius); + padding: 2px var(--sp-2); + font-size: var(--fs-xs); + line-height: 1.4; + cursor: pointer; + color: var(--muted); + white-space: nowrap; +} + +.copy-btn:hover { + color: var(--text); + border-color: var(--muted); +} + +.copy-btn[data-copied] { + color: var(--ok); + border-color: var(--ok); +} + +.hash { + display: inline-flex; + align-items: baseline; + gap: var(--sp-2); + max-width: 100%; +} + +.hash-value { + font-family: var(--font-mono); + overflow-wrap: anywhere; + word-break: break-all; +} + +.badge { + display: inline-block; + border-radius: 999px; + padding: 1px var(--sp-2); + font-size: var(--fs-xs); + line-height: 1.5; + border: 1px solid var(--border); + background: var(--surface); + color: var(--muted); + white-space: nowrap; +} + +.badge-accent { + color: var(--accent); + background: var(--accent-soft); + border-color: transparent; +} + +.badge-ok { + color: var(--ok); + background: var(--ok-soft); + border-color: transparent; +} + +.badge-warn { + color: var(--warn); + background: var(--warn-soft); + border-color: transparent; +} + +.badge-danger { + color: var(--danger); + background: var(--danger-soft); + border-color: transparent; +} + +.badge-link:hover { + text-decoration: none; +} + +.state { + border: 1px dashed var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-8); + text-align: center; + color: var(--muted); +} + +.state-title { + color: var(--text); + font-weight: 600; + margin: 0 0 var(--sp-2); +} + +.state-error { + border-color: var(--danger); +} + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-5); +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/site/src/styles/tokens.css b/apps/site/src/styles/tokens.css new file mode 100644 index 0000000..5f2842c --- /dev/null +++ b/apps/site/src/styles/tokens.css @@ -0,0 +1,63 @@ +:root { + color-scheme: dark light; + + --bg: #0b0d10; + --surface: #12151a; + --surface-2: #1a1f26; + --border: #262c35; + --text: #e6e8eb; + --muted: #8b949e; + --accent: #5298ff; + --accent-soft: rgba(82, 152, 255, 0.14); + --ok: #3fb950; + --ok-soft: rgba(63, 185, 80, 0.14); + --warn: #d29922; + --warn-soft: rgba(210, 153, 34, 0.14); + --danger: #f85149; + --danger-soft: rgba(248, 81, 73, 0.14); + + --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; + --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + + --fs-xs: 13px; + --fs-sm: 14px; + --fs-md: 16px; + --fs-lg: 20px; + --fs-xl: 28px; + --fs-2xl: 40px; + + --radius: 6px; + --radius-lg: 10px; + + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 20px; + --sp-6: 24px; + --sp-8: 32px; + --sp-10: 40px; + --sp-12: 48px; + --sp-16: 64px; + + --content-max: 1120px; +} + +@media (prefers-color-scheme: light) { + :root { + --bg: #ffffff; + --surface: #f6f8fa; + --surface-2: #eaeef2; + --border: #d0d7de; + --text: #1f2328; + --muted: #59636e; + --accent: #0969da; + --accent-soft: rgba(9, 105, 218, 0.1); + --ok: #1a7f37; + --ok-soft: rgba(26, 127, 55, 0.1); + --warn: #9a6700; + --warn-soft: rgba(154, 103, 0, 0.1); + --danger: #cf222e; + --danger-soft: rgba(207, 34, 46, 0.1); + } +} diff --git a/apps/site/src/vite-env.d.ts b/apps/site/src/vite-env.d.ts new file mode 100644 index 0000000..52ea523 --- /dev/null +++ b/apps/site/src/vite-env.d.ts @@ -0,0 +1,8 @@ +/// + +interface ImportMetaEnv { + readonly VITE_INDEX_URL?: string; + readonly VITE_REGISTRAR_URL?: string; + readonly VITE_CHAIN?: string; + readonly VITE_DATA_SOURCE?: string; +} diff --git a/apps/site/test/fixtures/attestation-jeff.json b/apps/site/test/fixtures/attestation-jeff.json new file mode 100644 index 0000000..aad442a --- /dev/null +++ b/apps/site/test/fixtures/attestation-jeff.json @@ -0,0 +1,17 @@ +{ + "label": "jeff", + "name": "jeff.enspack.eth", + "hfNamespace": "jefflau", + "address": "0x14ac0369ddef2ce89577f19207f84a1fb764d543", + "claimId": "clm_eae6dce4d473c56a", + "challenge": "enspack-verify:6b284480a6dda9b7867f94e86a035fe5b93d7130d2044d4ca7aea5d846981469", + "repo": "jefflau/enspack-verify", + "commit": "a56625b5541b33f6b1842637965888e8a15b470b", + "signature": "0xd368b0e8bbbd4a474a846a5fbfab4d8cfc1b3f8f413d25dbce5605f3fd38bbc260af54755aae952ac69e62de639b238540c593d6d3e6e9cc992768ff150fdc987f", + "txs": [ + "0x4f804f611d05209bb3c24ed4275733d27ed5f5198c5bfe023dabe3da835d82ff", + "0x4d97664a0b4d68b03261abd7a5683ac97bdc093430699f676c514d3bc96ba014" + ], + "issuedAt": "2026-09-16T09:41:00Z", + "chain": "sepolia" +} diff --git a/apps/site/test/fixtures/name-jeff.json b/apps/site/test/fixtures/name-jeff.json new file mode 100644 index 0000000..5f26a48 --- /dev/null +++ b/apps/site/test/fixtures/name-jeff.json @@ -0,0 +1,86 @@ +{ + "model": "tiny-random.jeff.enspack.eth", + "versions": [ + { + "name": "v0-1-0.tiny-random.jeff.enspack.eth", + "version": "0.1.0", + "cid": "bafkreiq2jzse7l6h3bz37newkvc5qkjog6b6slmbt7smopt333icaq4ipl", + "createdAt": "2026-09-16T10:00:00Z" + }, + { + "name": "v0-2-0.tiny-random.jeff.enspack.eth", + "version": "0.2.0", + "cid": "bafkreiqlqo4tavkcgykxj22n246t6z462d45spaqezclkcxnqw3dryt4sx", + "createdAt": "2026-09-18T14:12:00Z" + } + ], + "manifest": { + "spec": "enspack/0.1", + "name": "v0-2-0.tiny-random.jeff.enspack.eth", + "model": "tiny-random.jeff.enspack.eth", + "publisher": "jeff.enspack.eth", + "version": "0.2.0", + "createdAt": "2026-09-18T14:12:00Z", + "displayName": "tiny-random", + "license": "mit", + "licenseUrl": "https://huggingface.co/jefflau/tiny-random/blob/3f0c2a9d1e8b7c6a5d4e3f2a1b0c9d8e7f6a5b4c/LICENSE", + "upstream": { + "provider": "huggingface", + "repo": "jefflau/tiny-random", + "url": "https://huggingface.co/jefflau/tiny-random", + "revision": "3f0c2a9d1e8b7c6a5d4e3f2a1b0c9d8e7f6a5b4c" + }, + "distribution": { + "infohash": "cc14c60026a418e23e1a2ad0c1aaee22ed4439f2", + "webseeds": [ + "https://huggingface.co/jefflau/tiny-random/resolve/3f0c2a9d1e8b7c6a5d4e3f2a1b0c9d8e7f6a5b4c/" + ], + "torrent": { + "cid": "bafkrei5j7377yemorjn7fik7ktjkd76htfwpqjftzj5hnk7m45qcmhsi6z" + }, + "magnet": "magnet:?xt=urn:btih:cc14c60026a418e23e1a2ad0c1aaee22ed4439f2&dn=tiny-random" + }, + "files": [ + { + "path": "config.json", + "size": 612, + "sha256": "d34ddab30f9487b9ede3e550786d30eeec2285e2e2a9e7a5e66084b47fe99d65", + "role": "config" + }, + { + "path": "model.safetensors", + "size": 4194512, + "sha256": "ecad3aaece44f0d2180b09308aefb49eea7aa4f3a970b0df190deb54c78a410f", + "role": "weight" + }, + { + "path": "tokenizer.json", + "size": 2113, + "sha256": "f0e21a1b1c7d365985647c38e75d7ff8e4578955da446cac9ca7ebcd0d3234b4", + "role": "tokenizer" + }, + { + "path": "README.md", + "size": 1420, + "sha256": "2ea30fe5d1f68bb891d692993de79d89dc513064a2e3f641b844bd333e8a7acd", + "role": "doc" + } + ], + "totalSize": 4198657, + "versions": [ + { + "version": "0.1.0", + "name": "v0-1-0.tiny-random.jeff.enspack.eth", + "cid": "bafkreiq2jzse7l6h3bz37newkvc5qkjog6b6slmbt7smopt333icaq4ipl", + "createdAt": "2026-09-16T10:00:00Z" + }, + { + "version": "0.2.0", + "name": "v0-2-0.tiny-random.jeff.enspack.eth", + "cid": "bafkreiqlqo4tavkcgykxj22n246t6z462d45spaqezclkcxnqw3dryt4sx", + "createdAt": "2026-09-18T14:12:00Z" + } + ], + "previous": "bafkreiq2jzse7l6h3bz37newkvc5qkjog6b6slmbt7smopt333icaq4ipl" + } +} diff --git a/apps/site/test/fixtures/name-llama.json b/apps/site/test/fixtures/name-llama.json new file mode 100644 index 0000000..e34132b --- /dev/null +++ b/apps/site/test/fixtures/name-llama.json @@ -0,0 +1,97 @@ +{ + "model": "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "versions": [ + { + "name": "v1-0-0.meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "version": "1.0.0", + "cid": "bafkreioovluxxz2mgsituvkognp33nuccuxtsn7kiwa5pvisywiy2rarrh", + "createdAt": "2026-09-15T08:30:00Z" + } + ], + "manifest": { + "spec": "enspack/0.1", + "name": "v1-0-0.meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "model": "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "publisher": "mirrors.enspack.eth", + "version": "1.0.0", + "createdAt": "2026-09-15T08:30:00Z", + "displayName": "Llama-3.2-1B-Instruct", + "license": "llama3.2", + "licenseUrl": "https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/blob/9213176726f574b556790deb65791e0c5aa438b6/LICENSE", + "upstream": { + "provider": "huggingface", + "repo": "meta-llama/Llama-3.2-1B-Instruct", + "url": "https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct", + "revision": "9213176726f574b556790deb65791e0c5aa438b6" + }, + "distribution": { + "infohash": "2261eac56b458d3a2720ff6f5e5e5cb6de75649e", + "webseeds": [ + "https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/resolve/9213176726f574b556790deb65791e0c5aa438b6/" + ], + "torrent": { + "cid": "bafkreiari6rfjs3w7ozmozlpllnsirjwtfv54lkqnf3nmpzaaf7lsztciq" + }, + "magnet": "magnet:?xt=urn:btih:2261eac56b458d3a2720ff6f5e5e5cb6de75649e&dn=Llama-3.2-1B-Instruct" + }, + "files": [ + { + "path": "config.json", + "size": 877, + "sha256": "b2adbeb198827c155f321d14a71864793e42e9c7b3784be7e91e362f4c8fbe1d", + "role": "config" + }, + { + "path": "generation_config.json", + "size": 189, + "sha256": "07973cd474e5f28ce207287f5c8ed0435d08fa7952903212e212cf9ba84311b5", + "role": "config" + }, + { + "path": "model.safetensors", + "size": 2471645608, + "sha256": "3afb8e36158ca06f20f7555c526835fd62db6a460189de52c0e88eca3f4c41c6", + "role": "weight" + }, + { + "path": "special_tokens_map.json", + "size": 296, + "sha256": "732b4fcb3f140f5afe4849a9fc9cc076b5604192bafbcdef22bb4694b8cfa5ab", + "role": "tokenizer" + }, + { + "path": "tokenizer.json", + "size": 9085657, + "sha256": "1de82e2af146b4ce8a57f05e6a890bd4a453e18b898842bb0d99e5fe7af917b3", + "role": "tokenizer" + }, + { + "path": "tokenizer_config.json", + "size": 54528, + "sha256": "be27ee438dcd533a36a44cf9a74eff034dd1226aab5d765ce5fe2efe7e30f74b", + "role": "tokenizer" + }, + { + "path": "LICENSE.txt", + "size": 7712, + "sha256": "bb0e95fae6937efdbba43f7fb0562ccd0b521b7fcb396a20e8b33a94286933f9", + "role": "license" + }, + { + "path": "README.md", + "size": 41742, + "sha256": "2f0f009369df31606499eb9b862ced426d6ef3b0dc4953a49e4446cfaf89b77c", + "role": "doc" + } + ], + "totalSize": 2480836609, + "versions": [ + { + "version": "1.0.0", + "name": "v1-0-0.meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "cid": "bafkreioovluxxz2mgsituvkognp33nuccuxtsn7kiwa5pvisywiy2rarrh", + "createdAt": "2026-09-15T08:30:00Z" + } + ] + } +} diff --git a/apps/site/test/fixtures/name-qwen.json b/apps/site/test/fixtures/name-qwen.json new file mode 100644 index 0000000..ccfec22 --- /dev/null +++ b/apps/site/test/fixtures/name-qwen.json @@ -0,0 +1,140 @@ +{ + "model": "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "versions": [ + { + "version": "1.0.0", + "name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "cid": "bafkreis5hm6e3jayscwqnqudbrnems5iuqts36dojc5hgq5yuvxl4vg42c", + "createdAt": "2026-09-14T00:00:00Z" + } + ], + "manifest": { + "spec": "enspack/0.1", + "name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "model": "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "publisher": "mirrors.enspack.eth", + "version": "1.0.0", + "createdAt": "2026-09-14T00:00:00Z", + "displayName": "Qwen2.5-7B-Instruct", + "license": "apache-2.0", + "licenseUrl": "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/blob/a09a35458c702b33eeacc393d103063234e8bc28/LICENSE", + "upstream": { + "provider": "huggingface", + "repo": "Qwen/Qwen2.5-7B-Instruct", + "url": "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct", + "revision": "a09a35458c702b33eeacc393d103063234e8bc28" + }, + "canonical": "qwen2-5-7b-instruct.qwen.enspack.eth", + "distribution": { + "infohash": "136cb94838da83a906263ec23d03dce95d736c64", + "magnet": "magnet:?xt=urn:btih:136cb94838da83a906263ec23d03dce95d736c64&dn=Qwen2.5-7B-Instruct", + "torrent": { + "cid": "bafkreii7g5a56h67x3lapfb5y4hqi6s4zsk54ttcent2g5e54bl3refik3", + "url": "https://seed1.enspack.dev/torrents/qwen--qwen2-5-7b-instruct-v1-0-0.torrent" + }, + "webseeds": [ + "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/resolve/a09a35458c702b33eeacc393d103063234e8bc28/", + "https://huggingbay.xyz/api/downloads/hf-model-qwen-qwen2-5-7b-instruct/" + ], + "hb": "hb://Qwen/Qwen2.5-7B-Instruct@sha256:d82247b7101aea36fd25bc61273bc1d2627d2cc0cb0fd29cb9586044f4e999fd", + "seeds": [ + "seed1.enspack.eth:6881" + ] + }, + "files": [ + { + "path": ".gitattributes", + "size": 1519, + "sha256": "11ad7efa24975ee4b0c3c3a38ed18737f0658a5f75a0a96787b576a78a023361", + "role": "other" + }, + { + "path": "LICENSE", + "size": 11343, + "sha256": "832dd9e00a68dd83b3c3fb9f5588dad7dcf337a0db50f7d9483f310cd292e92e", + "role": "license" + }, + { + "path": "README.md", + "size": 6240, + "sha256": "f366f33bbf6bcadbb7d87f0a21a7b65584a56b8d58b0743c77c88bee625b93a6", + "role": "doc" + }, + { + "path": "config.json", + "size": 663, + "sha256": "7463bb0ea78315365e6c6b74de4e73bbcc8359dfb0c5a737584e077d42c0b03c", + "role": "config" + }, + { + "path": "generation_config.json", + "size": 243, + "sha256": "3a8f9087e486054c8a4a08dae2e5a3ba62e23da212b5b8c08bc42cb983c3459f", + "role": "config" + }, + { + "path": "merges.txt", + "size": 1671839, + "sha256": "599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3", + "role": "tokenizer" + }, + { + "path": "model-00001-of-00004.safetensors", + "size": 3945441440, + "sha256": "a1333e6293854747c481288ea83b348226af178dd565c49b6f9495ba1966aba7", + "role": "weight" + }, + { + "path": "model-00002-of-00004.safetensors", + "size": 3864726352, + "sha256": "f5d25a2772cb825164a2a2c0fb6d51a87e282abf21e4dd75bc5cfb3cd0ea6185", + "role": "weight" + }, + { + "path": "model-00003-of-00004.safetensors", + "size": 3864726424, + "sha256": "8efdec4c1bc12317ae1a38dc42b595ce777738a64deea3fcb8a0a91381bcdfd5", + "role": "weight" + }, + { + "path": "model-00004-of-00004.safetensors", + "size": 3556377672, + "sha256": "1a72d403cdf0c1ec3cb7f289f17b394a01e64394c2e9b3c0f94dbce3faf879bd", + "role": "weight" + }, + { + "path": "model.safetensors.index.json", + "size": 27752, + "sha256": "624bf7c47cd12468fdc16e38a47cf4f19e0415b859a223ba3c027eed2f0e1028", + "role": "index" + }, + { + "path": "tokenizer.json", + "size": 7031645, + "sha256": "c0382117ea329cdf097041132f6d735924b697924d6f6fc3945713e96ce87539", + "role": "tokenizer" + }, + { + "path": "tokenizer_config.json", + "size": 7305, + "sha256": "5b5d4f65d0acd3b2d56a35b56d374a36cbc1c8fa5cf3b3febbbfabf22f359583", + "role": "tokenizer" + }, + { + "path": "vocab.json", + "size": 2776833, + "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910", + "role": "tokenizer" + } + ], + "totalSize": 15242807270, + "versions": [ + { + "version": "1.0.0", + "name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "cid": "bafkreis5hm6e3jayscwqnqudbrnems5iuqts36dojc5hgq5yuvxl4vg42c", + "createdAt": "2026-09-14T00:00:00Z" + } + ] + } +} diff --git a/apps/site/test/fixtures/names.json b/apps/site/test/fixtures/names.json new file mode 100644 index 0000000..787850f --- /dev/null +++ b/apps/site/test/fixtures/names.json @@ -0,0 +1,41 @@ +{ + "items": [ + { + "model": "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "publisher": "mirrors.enspack.eth", + "latest": { + "name": "v1-0-0.meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + "version": "1.0.0", + "cid": "bafkreioovluxxz2mgsituvkognp33nuccuxtsn7kiwa5pvisywiy2rarrh" + }, + "upstream": "meta-llama/Llama-3.2-1B-Instruct", + "license": "llama3.2", + "totalSize": 2480836609 + }, + { + "model": "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "publisher": "mirrors.enspack.eth", + "latest": { + "name": "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth", + "version": "1.0.0", + "cid": "bafkreis5hm6e3jayscwqnqudbrnems5iuqts36dojc5hgq5yuvxl4vg42c" + }, + "upstream": "Qwen/Qwen2.5-7B-Instruct", + "license": "apache-2.0", + "totalSize": 15242807270 + }, + { + "model": "tiny-random.jeff.enspack.eth", + "publisher": "jeff.enspack.eth", + "latest": { + "name": "v0-2-0.tiny-random.jeff.enspack.eth", + "version": "0.2.0", + "cid": "bafkreiqlqo4tavkcgykxj22n246t6z462d45spaqezclkcxnqw3dryt4sx" + }, + "upstream": "jefflau/tiny-random", + "license": "mit", + "totalSize": 4198657 + } + ], + "nextCursor": null +} diff --git a/apps/site/test/fixtures/publishers.json b/apps/site/test/fixtures/publishers.json new file mode 100644 index 0000000..9e40072 --- /dev/null +++ b/apps/site/test/fixtures/publishers.json @@ -0,0 +1,14 @@ +{ + "items": [ + { + "name": "jeff.enspack.eth", + "hf": "jefflau", + "models": 1 + }, + { + "name": "mirrors.enspack.eth", + "hf": null, + "models": 2 + } + ] +} diff --git a/apps/site/test/fixtures/violations-one.json b/apps/site/test/fixtures/violations-one.json new file mode 100644 index 0000000..9f9dfe2 --- /dev/null +++ b/apps/site/test/fixtures/violations-one.json @@ -0,0 +1,11 @@ +{ + "items": [ + { + "name": "v0-1-0.tiny-random.jeff.enspack.eth", + "node": "0xe06ee3b0143f362de535730222149d6448db717552464f2cb79cf5e145bd9162", + "previousCid": "bafkreiq2jzse7l6h3bz37newkvc5qkjog6b6slmbt7smopt333icaq4ipl", + "newCid": "bafkreiz7kc52q2kk5jepgmkk3grbtroft46lfgvwqelbrfpkp4vgqm4kit", + "block": 9123456 + } + ] +} diff --git a/apps/site/test/fixtures/violations.json b/apps/site/test/fixtures/violations.json new file mode 100644 index 0000000..2feb210 --- /dev/null +++ b/apps/site/test/fixtures/violations.json @@ -0,0 +1,3 @@ +{ + "items": [] +} diff --git a/apps/site/test/helpers.tsx b/apps/site/test/helpers.tsx new file mode 100644 index 0000000..0555cc0 --- /dev/null +++ b/apps/site/test/helpers.tsx @@ -0,0 +1,30 @@ +import { type RenderResult, render } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { MemoryRouter, Route, Routes } from "react-router"; +import { demoFixtures } from "../src/demo/fixtures.js"; +import { ClientProvider } from "../src/lib/client-context.js"; +import { FixtureIndexClient, type IndexClient } from "../src/lib/client.js"; + +export { demoFixtures }; + +export function fixtureClient(): IndexClient { + return new FixtureIndexClient(demoFixtures); +} + +/** Renders `ui` under a router at `path`, with the fixture client (or a custom one). */ +export function renderAt( + path: string, + ui: ReactNode, + opts: { client?: IndexClient; routePattern?: string } = {}, +): RenderResult { + const client = opts.client ?? fixtureClient(); + return render( + + + + + + + , + ); +} diff --git a/apps/site/test/lib.test.ts b/apps/site/test/lib.test.ts new file mode 100644 index 0000000..7124bb6 --- /dev/null +++ b/apps/site/test/lib.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from "vitest"; +import { ApiError } from "../src/lib/api.js"; +import { FixtureIndexClient, HttpIndexClient } from "../src/lib/client.js"; +import { readConfig } from "../src/lib/config.js"; +import { ensAppUrl, firstLabel, formatBytes, shortHex } from "../src/lib/format.js"; +import { demoFixtures } from "./helpers.js"; + +describe("config", () => { + it("defaults to sepolia + http and strips trailing slashes", () => { + const c = readConfig({ VITE_INDEX_URL: "https://idx.example/" } as ImportMetaEnv); + expect(c.chain).toBe("sepolia"); + expect(c.dataSource).toBe("http"); + expect(c.indexUrl).toBe("https://idx.example"); + }); + + it("honours mainnet + demo", () => { + const c = readConfig({ VITE_CHAIN: "mainnet", VITE_DATA_SOURCE: "demo" } as ImportMetaEnv); + expect(c.chain).toBe("mainnet"); + expect(c.dataSource).toBe("demo"); + }); +}); + +describe("format", () => { + it("formats bytes in decimal units", () => { + expect(formatBytes(663)).toBe("663 B"); + expect(formatBytes(15242807270)).toBe("15.2 GB"); + expect(formatBytes(2471645608)).toBe("2.47 GB"); + }); + it("shortens hex and keeps short values intact", () => { + expect(shortHex("0x1234567890abcdef")).toBe("0x1234…cdef"); + expect(shortHex("abc")).toBe("abc"); + }); + it("derives labels and ENS app urls per chain", () => { + expect(firstLabel("jeff.enspack.eth")).toBe("jeff"); + expect(ensAppUrl("sepolia", "x.eth")).toBe("https://sepolia.app.ens.domains/x.eth"); + expect(ensAppUrl("mainnet", "x.eth")).toBe("https://app.ens.domains/x.eth"); + }); +}); + +describe("FixtureIndexClient", () => { + const client = new FixtureIndexClient(demoFixtures); + + it("lists, filters by publisher and searches by display name", async () => { + expect((await client.listNames()).items).toHaveLength(3); + const mirrors = await client.listNames({ publisher: "mirrors.enspack.eth" }); + expect(mirrors.items.map((i) => i.publisher)).toEqual([ + "mirrors.enspack.eth", + "mirrors.enspack.eth", + ]); + const qwen = await client.listNames({ q: "Qwen2.5" }); + expect(qwen.items).toHaveLength(1); + }); + + it("resolves a version name to its model detail and 404s unknown names", async () => { + const d = await client.getName("v0-1-0.tiny-random.jeff.enspack.eth"); + expect(d.model).toBe("tiny-random.jeff.enspack.eth"); + await expect(client.getName("nope.eth")).rejects.toMatchObject({ status: 404 }); + }); + + it("returns null attestation for non-registrar publishers", async () => { + expect(await client.getAttestation("mirrors")).toBeNull(); + expect(await client.getAttestation("jeff")).not.toBeNull(); + }); +}); + +describe("HttpIndexClient", () => { + it("builds query strings and surfaces indexer error codes", async () => { + const calls: string[] = []; + const fetchImpl = vi.fn(async (input: RequestInfo | URL) => { + calls.push(String(input)); + return new Response(JSON.stringify({ error: "invalid cursor", code: "INVALID" }), { + status: 400, + headers: { "content-type": "application/json" }, + }); + }) as unknown as typeof fetch; + const client = new HttpIndexClient("https://idx", "https://reg", fetchImpl); + const err = await client.listNames({ q: "a b", limit: 5 }).catch((e: unknown) => e); + expect(err).toBeInstanceOf(ApiError); + expect((err as ApiError).code).toBe("INVALID"); + expect(calls[0]).toBe("https://idx/v1/names?q=a+b&limit=5"); + }); + + it("maps registrar 404 to null and network failure to NETWORK", async () => { + const notFound = (async () => new Response("{}", { status: 404 })) as unknown as typeof fetch; + const client = new HttpIndexClient("https://idx", "https://reg", notFound); + expect(await client.getAttestation("x")).toBeNull(); + const down = (async () => { + throw new TypeError("Failed to fetch"); + }) as unknown as typeof fetch; + const c2 = new HttpIndexClient("https://idx", "https://reg", down); + await expect(c2.listPublishers()).rejects.toMatchObject({ status: 0, code: "NETWORK" }); + }); +}); diff --git a/apps/site/test/setup.ts b/apps/site/test/setup.ts new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/apps/site/test/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json new file mode 100644 index 0000000..6b1e48c --- /dev/null +++ b/apps/site/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "noEmit": true, + "declaration": false, + "types": ["vite/client", "@testing-library/jest-dom"] + }, + "include": ["src", "test", "vite.config.ts", "vitest.config.ts"] +} diff --git a/apps/site/vite.config.ts b/apps/site/vite.config.ts new file mode 100644 index 0000000..2911915 --- /dev/null +++ b/apps/site/vite.config.ts @@ -0,0 +1,7 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + build: { target: "es2022", sourcemap: true }, +}); diff --git a/apps/site/vitest.config.ts b/apps/site/vitest.config.ts new file mode 100644 index 0000000..4452601 --- /dev/null +++ b/apps/site/vitest.config.ts @@ -0,0 +1,11 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [react()], + test: { + environment: "jsdom", + setupFiles: ["./test/setup.ts"], + include: ["test/**/*.test.{ts,tsx}"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 131428d..ad8b448 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,50 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) + + apps/site: + dependencies: + react: + specifier: ^19.1.0 + version: 19.3.0 + react-dom: + specifier: ^19.1.0 + version: 19.3.0(react@19.3.0) + react-router: + specifier: ^7.6.0 + version: 7.18.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + devDependencies: + '@enspack/core': + specifier: workspace:* + version: link:../../packages/core + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.10.0(@testing-library/dom@10.4.2) + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@types/react': + specifier: ^19.1.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.1.0 + version: 19.3.0(@types/react@19.3.0) + '@vitejs/plugin-react': + specifier: ^4.4.0 + version: 4.7.0(vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1)) + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vite: + specifier: ^6.3.0 + version: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) + vitest: + specifier: ^3.0.0 + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) bootstrap: dependencies: @@ -56,7 +99,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) infra: devDependencies: @@ -102,7 +145,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) packages/core: dependencies: @@ -145,7 +188,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) packages/hf: dependencies: @@ -164,7 +207,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) packages/torrent: dependencies: @@ -198,7 +241,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) services/indexer: dependencies: @@ -223,7 +266,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) services/registrar: dependencies: @@ -263,7 +306,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) services/seed: dependencies: @@ -291,7 +334,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) yaml: specifier: ^2.7.0 version: 2.9.1 @@ -319,10 +362,13 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} @@ -330,14 +376,96 @@ packages: resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} engines: {node: '>=14.21.3'} @@ -396,6 +524,34 @@ packages: peerDependencies: commander: ~12.1.0 + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@electric-sql/pglite@0.2.13': resolution: {integrity: sha512-YRY806NnScVqa21/1L1vaysSQ+0/cAva50z7vlwzaGiBOTS9JhdzIRHN0KfgMhobFAphbznZJ7urMso4RtMBIQ==} @@ -424,14 +580,14 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.2': - resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -442,14 +598,14 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.28.2': - resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -460,14 +616,14 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.2': - resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -478,14 +634,14 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.2': - resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -496,14 +652,14 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.2': - resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -514,14 +670,14 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.28.2': - resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -532,14 +688,14 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.28.2': - resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -550,14 +706,14 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.2': - resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -568,14 +724,14 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.28.2': - resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -586,14 +742,14 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.2': - resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -604,14 +760,14 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.2': - resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -622,14 +778,14 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.2': - resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -640,14 +796,14 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.28.2': - resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -658,14 +814,14 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.28.2': - resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -676,14 +832,14 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.28.2': - resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -694,14 +850,14 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.28.2': - resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -712,26 +868,26 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.2': - resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.2': - resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -742,26 +898,26 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.2': - resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.28.2': - resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -772,26 +928,26 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.2': - resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.28.2': - resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -802,14 +958,14 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.28.2': - resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -820,14 +976,14 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.2': - resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -838,14 +994,14 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.28.2': - resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -856,14 +1012,14 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.28.2': - resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -962,6 +1118,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1016,6 +1175,9 @@ packages: '@repeaterjs/repeater@3.1.0': resolution: {integrity: sha512-TaoVksZRSx2KWYYpyLQtMQXXeS98VsgZImzW65xmiVgbYhXLk+aEsmzPLirqVuE4/XuUapH2iMtxUzaBNDzdSQ==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.63.2': resolution: {integrity: sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==} cpu: [arm] @@ -1150,10 +1312,51 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.10.0': + resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + deprecated: Incorrect minor release with breaking changes (Node >=22 and required @testing-library/dom peer). Use 6.9.1 for the 6.x line, or upgrade to 7.0.0. + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@thaunknown/thirty-two@1.0.6': resolution: {integrity: sha512-Uz2kFb0cNeHg7hds8BtI/umcwzaSMUdbkeQgZrIZQbp3gWRFjrvrvBHNEB6SQYChVxC3rWHovKx1Ya/vSRRMYA==} engines: {node: '>=0.2.6'} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1178,6 +1381,20 @@ packages: '@types/picomatch@3.0.2': resolution: {integrity: sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==} + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/expect@3.2.7': resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} @@ -1262,6 +1479,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1297,6 +1518,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1307,6 +1532,13 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1324,6 +1556,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.11.23: + resolution: {integrity: sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ==} + engines: {node: '>=6.0.0'} + hasBin: true + bencode@4.0.1: resolution: {integrity: sha512-/bCR3FvgfB8AZk4LlowShU+nfaTNRM2vruJ9aejDa5LpgkCoRjFyEauFiZFxbpxVxQbBFapYCN42Er2dkE/Qgw==} engines: {node: '>=12.20.0'} @@ -1341,6 +1578,11 @@ packages: brace-expansion@2.1.4: resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -1354,6 +1596,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1392,6 +1637,13 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copy-anything@4.1.0: resolution: {integrity: sha512-ufbM3smX/Jbnpk5wcQjzd1MgBpzmqfNETUAyZNrGwU9foRlyHoGzMMBBCRzEhQLBjZfFDE1W2ufPXX2vdWkV8Q==} engines: {node: '>=18'} @@ -1412,10 +1664,24 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + dataloader@2.2.3: resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} @@ -1432,6 +1698,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1440,10 +1709,20 @@ packages: resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} engines: {node: '>=10'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-package-manager@3.0.2: resolution: {integrity: sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==} engines: {node: '>=12'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dot-prop@8.0.2: resolution: {integrity: sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==} engines: {node: '>=16'} @@ -1544,12 +1823,19 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.427: + resolution: {integrity: sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1566,16 +1852,20 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true - esbuild@0.28.2: - resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1648,6 +1938,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-stdin@10.0.0: resolution: {integrity: sha512-eWSePJ4zXFdqz+/Lyfopob4rIcoF/U2XfE8nJc7iZV6lnebWc9k7DoQQpX+2a9jc0AOvBsXvbe5YkjXl/MHbpg==} engines: {node: '>=20'} @@ -1682,17 +1976,37 @@ packages: resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} engines: {node: '>=16.9.0'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + http-terminator@3.2.0: resolution: {integrity: sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==} engines: {node: '>=14'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + ipfs-unixfs@13.1.0: resolution: {integrity: sha512-gT6c9ii/OiPDs1l+7VZDFe/sTNWLbGCUSd9FaZGCHNnO48xkyTTOMl3+K83+qhc/os1DCYKNkf36db5XeG4VFQ==} @@ -1711,6 +2025,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1746,6 +2063,20 @@ packages: resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-schema-to-typescript@15.0.4: resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==} engines: {node: '>=16.0.0'} @@ -1757,6 +2088,11 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + junk@4.0.1: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} @@ -1785,6 +2121,13 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1803,6 +2146,10 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -1843,10 +2190,17 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-releases@2.0.55: + resolution: {integrity: sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==} + engines: {node: '>=18'} + npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nwsapi@2.2.27: + resolution: {integrity: sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1893,6 +2247,9 @@ packages: engines: {node: '>=12.20.0'} hasBin: true + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2033,6 +2390,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} @@ -2048,6 +2409,10 @@ packages: protons-runtime@7.1.0: resolution: {integrity: sha512-TAL8CpKEUK+k0KKZIQrDTBgyH86zZAjz3ZULcCjxrb9zvkTVEGJyVbATmTV4KpZmFFEH7ERWDNWxfiE+N3bOUQ==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -2057,6 +2422,32 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router@7.18.3: + resolution: {integrity: sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + readable-stream@4.7.0: resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2069,6 +2460,10 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2086,6 +2481,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2096,14 +2494,31 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2170,6 +2585,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -2188,6 +2607,9 @@ packages: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tdigest@0.1.3: resolution: {integrity: sha512-zbRt+lT+/H4fRItHshczHErVCQnitJk8MfMT24MqFJf3YL7SJJPqGIGeuOdvxXxM/AHFzKBl7WoyaYwqO9s3Kw==} @@ -2227,6 +2649,21 @@ packages: resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2306,6 +2743,12 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + urlpattern-polyfill@10.1.0: resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} @@ -2366,19 +2809,19 @@ packages: terser: optional: true - vite@7.3.6: - resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} - engines: {node: ^20.19.0 || >=22.12.0} + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: '>=1.21.0' - less: ^4.0.0 + less: '*' lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 @@ -2434,10 +2877,31 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} @@ -2474,10 +2938,20 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.9.1: resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} engines: {node: '>= 14.6'} @@ -2485,6 +2959,8 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + '@adraffy/ens-normalize@1.11.1': {} '@apidevtools/json-schema-ref-parser@11.9.3': @@ -2493,14 +2969,128 @@ snapshots: '@types/json-schema': 7.0.15 js-yaml: 4.3.2 + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.9 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -2540,6 +3130,26 @@ snapshots: dependencies: commander: 12.1.0 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + '@electric-sql/pglite@0.2.13': {} '@electric-sql/pglite@0.3.16': {} @@ -2569,226 +3179,226 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/aix-ppc64@0.27.7': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.28.2': + '@esbuild/aix-ppc64@0.27.7': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm64@0.27.7': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.28.2': + '@esbuild/android-arm64@0.27.7': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-arm@0.27.7': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.28.2': + '@esbuild/android-arm@0.27.7': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/android-x64@0.27.7': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.28.2': + '@esbuild/android-x64@0.27.7': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.27.7': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.28.2': + '@esbuild/darwin-arm64@0.27.7': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/darwin-x64@0.27.7': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.28.2': + '@esbuild/darwin-x64@0.27.7': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.28.2': + '@esbuild/freebsd-arm64@0.27.7': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.27.7': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.28.2': + '@esbuild/freebsd-x64@0.27.7': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm64@0.27.7': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.28.2': + '@esbuild/linux-arm64@0.27.7': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.27.7': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.28.2': + '@esbuild/linux-arm@0.27.7': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-ia32@0.27.7': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.28.2': + '@esbuild/linux-ia32@0.27.7': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-loong64@0.27.7': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.28.2': + '@esbuild/linux-loong64@0.27.7': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-mips64el@0.27.7': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.28.2': + '@esbuild/linux-mips64el@0.27.7': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-ppc64@0.27.7': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.28.2': + '@esbuild/linux-ppc64@0.27.7': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.27.7': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.28.2': + '@esbuild/linux-riscv64@0.27.7': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-s390x@0.27.7': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.28.2': + '@esbuild/linux-s390x@0.27.7': optional: true '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/linux-x64@0.28.2': + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.28.2': + '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.21.5': + '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.28.2': + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.28.2': + '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.21.5': + '@esbuild/openbsd-x64@0.25.12': optional: true '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.28.2': + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.28.2': - optional: true - '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.27.7': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.28.2': + '@esbuild/sunos-x64@0.27.7': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-arm64@0.27.7': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.28.2': + '@esbuild/win32-arm64@0.27.7': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-ia32@0.27.7': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.28.2': + '@esbuild/win32-ia32@0.27.7': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@esbuild/win32-x64@0.27.7': + '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.28.2': + '@esbuild/win32-x64@0.27.7': optional: true '@escape.tech/graphql-armor-max-aliases@2.6.2': @@ -2912,6 +3522,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.6.0': {} @@ -2952,6 +3567,8 @@ snapshots: '@repeaterjs/repeater@3.1.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.63.2': optional: true @@ -3040,10 +3657,64 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.2)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.2 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.2 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + '@thaunknown/thirty-two@1.0.6': dependencies: uint8-util: 2.3.2 + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -3069,6 +3740,26 @@ snapshots: '@types/picomatch@3.0.2': {} + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.7': dependencies: '@types/chai': 5.2.3 @@ -3077,13 +3768,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.20.2)(yaml@2.9.1))': + '@vitest/mocker@3.2.7(vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@22.20.2)(yaml@2.9.1) + vite: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) '@vitest/pretty-format@3.2.7': dependencies: @@ -3158,6 +3849,8 @@ snapshots: acorn@8.18.0: {} + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -3185,12 +3878,20 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} any-promise@1.3.0: {} argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + assertion-error@2.0.1: {} atomic-sleep@1.0.0: {} @@ -3204,6 +3905,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.11.23: {} + bencode@4.0.1: dependencies: uint8-util: 2.3.2 @@ -3218,6 +3921,14 @@ snapshots: dependencies: balanced-match: 1.0.2 + browserslist@4.28.9: + dependencies: + baseline-browser-mapping: 2.11.23 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.427 + node-releases: 2.0.55 + update-browserslist-db: 1.3.3(browserslist@4.28.9) + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -3230,6 +3941,8 @@ snapshots: cac@6.7.14: {} + caniuse-lite@1.0.30001810: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3270,6 +3983,10 @@ snapshots: consola@3.4.2: {} + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + copy-anything@4.1.0: {} create-torrent@6.1.3: @@ -3302,8 +4019,22 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + dataloader@2.2.3: {} debounce-fn@5.1.2: @@ -3314,14 +4045,22 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deep-eql@5.0.2: {} delay@5.0.0: {} + dequal@2.0.3: {} + detect-package-manager@3.0.2: dependencies: execa: 5.1.1 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dot-prop@8.0.2: dependencies: type-fest: 3.13.1 @@ -3338,10 +4077,14 @@ snapshots: eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.427: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + entities@6.0.1: {} + env-paths@3.0.0: {} environment@1.1.0: {} @@ -3374,6 +4117,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -3403,34 +4175,7 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.2 - '@esbuild/android-arm': 0.28.2 - '@esbuild/android-arm64': 0.28.2 - '@esbuild/android-x64': 0.28.2 - '@esbuild/darwin-arm64': 0.28.2 - '@esbuild/darwin-x64': 0.28.2 - '@esbuild/freebsd-arm64': 0.28.2 - '@esbuild/freebsd-x64': 0.28.2 - '@esbuild/linux-arm': 0.28.2 - '@esbuild/linux-arm64': 0.28.2 - '@esbuild/linux-ia32': 0.28.2 - '@esbuild/linux-loong64': 0.28.2 - '@esbuild/linux-mips64el': 0.28.2 - '@esbuild/linux-ppc64': 0.28.2 - '@esbuild/linux-riscv64': 0.28.2 - '@esbuild/linux-s390x': 0.28.2 - '@esbuild/linux-x64': 0.28.2 - '@esbuild/netbsd-arm64': 0.28.2 - '@esbuild/netbsd-x64': 0.28.2 - '@esbuild/openbsd-arm64': 0.28.2 - '@esbuild/openbsd-x64': 0.28.2 - '@esbuild/openharmony-arm64': 0.28.2 - '@esbuild/sunos-x64': 0.28.2 - '@esbuild/win32-arm64': 0.28.2 - '@esbuild/win32-ia32': 0.28.2 - '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} estree-walker@3.0.3: dependencies: @@ -3497,6 +4242,8 @@ snapshots: fsevents@2.3.3: optional: true + gensync@1.0.0-beta.2: {} + get-stdin@10.0.0: {} get-stream@6.0.1: {} @@ -3534,6 +4281,17 @@ snapshots: hono@4.13.7: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http-terminator@3.2.0: dependencies: delay: 5.0.0 @@ -3541,10 +4299,23 @@ snapshots: roarr: 7.21.7 type-fest: 2.19.0 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} + indent-string@4.0.0: {} + ipfs-unixfs@13.1.0: dependencies: protons-runtime: 7.1.0 @@ -3561,6 +4332,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-potential-custom-element-name@1.0.1: {} + is-stream@2.0.1: {} isexe@2.0.0: {} @@ -3589,6 +4362,35 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.27 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + json-schema-to-typescript@15.0.4: dependencies: '@apidevtools/json-schema-ref-parser': 11.9.3 @@ -3605,6 +4407,8 @@ snapshots: json-schema-typed@8.0.2: {} + json5@2.2.3: {} + junk@4.0.1: {} kysely@0.26.3: {} @@ -3621,6 +4425,12 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -3637,6 +4447,8 @@ snapshots: mimic-fn@4.0.0: {} + min-indent@1.0.1: {} + minimatch@9.0.9: dependencies: brace-expansion: 2.1.4 @@ -3674,10 +4486,14 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-releases@2.0.55: {} + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 + nwsapi@2.2.27: {} + object-assign@4.1.1: {} obuf@1.1.2: {} @@ -3728,6 +4544,10 @@ snapshots: queue-microtask: 1.2.3 uint8-util: 2.3.2 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-key@3.1.1: {} path-scurry@1.11.1: @@ -3924,6 +4744,12 @@ snapshots: prettier@3.9.6: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + process-warning@3.0.0: {} process@0.11.10: {} @@ -3939,12 +4765,33 @@ snapshots: uint8arraylist: 3.0.2 uint8arrays: 6.1.1 + punycode@2.3.1: {} + pure-rand@6.1.0: {} queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react-is@17.0.2: {} + + react-refresh@0.17.0: {} + + react-router@7.18.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + cookie: 1.1.1 + react: 19.3.0 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.3.0(react@19.3.0) + + react@19.3.0: {} + readable-stream@4.7.0: dependencies: abort-controller: 3.0.0 @@ -3957,6 +4804,11 @@ snapshots: real-require@0.2.0: {} + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + require-from-string@2.0.2: {} resolve-from@5.0.0: {} @@ -3999,6 +4851,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.63.2 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4007,10 +4861,22 @@ snapshots: safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.28.0: {} + semver-compare@1.0.0: {} + semver@6.3.1: {} + semver@7.8.5: {} + set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4067,6 +4933,10 @@ snapshots: strip-final-newline@2.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -4091,6 +4961,8 @@ snapshots: dependencies: copy-anything: 4.1.0 + symbol-tree@3.2.4: {} + tdigest@0.1.3: dependencies: bintrees: 1.0.2 @@ -4124,6 +4996,20 @@ snapshots: tinyspy@4.0.6: {} + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-interface-checker@0.1.13: {} @@ -4191,6 +5077,12 @@ snapshots: undici-types@6.21.0: {} + update-browserslist-db@1.3.3(browserslist@4.28.9): + dependencies: + browserslist: 4.28.9 + escalade: 3.2.0 + picocolors: 1.1.1 + urlpattern-polyfill@10.1.0: {} viem@2.56.5(typescript@5.9.3): @@ -4234,7 +5126,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@22.20.2)(yaml@2.9.1) + vite: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) transitivePeerDependencies: - '@types/node' - jiti @@ -4269,9 +5161,9 @@ snapshots: '@types/node': 22.20.2 fsevents: 2.3.3 - vite@7.3.6(@types/node@22.20.2)(yaml@2.9.1): + vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1): dependencies: - esbuild: 0.28.2 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 postcss: 8.5.28 @@ -4282,11 +5174,11 @@ snapshots: fsevents: 2.3.3 yaml: 2.9.1 - vitest@3.2.7(@types/node@22.20.2)(yaml@2.9.1): + vitest@3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.2)(yaml@2.9.1)) + '@vitest/mocker': 3.2.7(vite@6.4.3(@types/node@22.20.2)(yaml@2.9.1)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -4304,11 +5196,12 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@22.20.2)(yaml@2.9.1) + vite: 6.4.3(@types/node@22.20.2)(yaml@2.9.1) vite-node: 3.2.4(@types/node@22.20.2)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.20.2 + jsdom: 26.1.0 transitivePeerDependencies: - jiti - less @@ -4323,8 +5216,25 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + web-streams-polyfill@3.3.3: {} + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + when-exit@2.1.5: {} which@2.0.2: @@ -4352,6 +5262,12 @@ snapshots: ws@8.21.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} + yallist@3.1.1: {} + yaml@2.9.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a2b26bb..1736b82 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - "packages/*" - "services/*" + - "apps/*" - "bootstrap" - "infra" - "test/e2e" From 5e4b21a5e288b36a6d101833b3f4121c8ca68f00 Mon Sep 17 00:00:00 2001 From: Jeff Lau Date: Mon, 14 Sep 2026 16:47:34 +0000 Subject: [PATCH 2/9] feat(indexer): allow cross-origin GET on /v1/* for the catalog site Co-authored-by: Jeff Lau --- services/indexer/CHANGELOG.md | 4 ++++ services/indexer/src/api/app.ts | 3 +++ services/indexer/test/api.test.ts | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/services/indexer/CHANGELOG.md b/services/indexer/CHANGELOG.md index 77916ad..f25dbbd 100644 --- a/services/indexer/CHANGELOG.md +++ b/services/indexer/CHANGELOG.md @@ -1,5 +1,9 @@ # @enspack/indexer +## Unreleased + +- CORS: `GET` / `OPTIONS` on `/v1/*` with `Access-Control-Allow-Origin: *` so the catalog site can call the API from `enspack.dev`. + ## 0.1.0 - WP-21: production Dockerfile copies `infra/package.json` so the workspace diff --git a/services/indexer/src/api/app.ts b/services/indexer/src/api/app.ts index d1f2f60..1ced376 100644 --- a/services/indexer/src/api/app.ts +++ b/services/indexer/src/api/app.ts @@ -1,5 +1,6 @@ import type { Manifest } from "@enspack/core"; import { Hono } from "hono"; +import { cors } from "hono/cors"; import { decodeNamesCursor, encodeNamesCursor } from "../cursor.js"; import type { IndexerReader, NameRow, VersionRow } from "../repo.js"; @@ -61,6 +62,8 @@ function preferName(rows: NameRow[]): NameRow | undefined { export function createIndexerApi(repo: IndexerReader): Hono { const app = new Hono(); + app.use("/v1/*", cors({ origin: "*", allowMethods: ["GET", "OPTIONS"] })); + app.get("/v1/health", (c) => c.json({ ok: true })); app.get("/v1/names", async (c) => { diff --git a/services/indexer/test/api.test.ts b/services/indexer/test/api.test.ts index 033dc85..f8df181 100644 --- a/services/indexer/test/api.test.ts +++ b/services/indexer/test/api.test.ts @@ -175,4 +175,22 @@ describe("API §4.3", () => { expect(res.status).toBe(200); expect(await res.json()).toEqual({ ok: true }); }); + + it("CORS: GET /v1/names allows any origin; OPTIONS preflight succeeds", async () => { + const { app } = await seededRepo(); + const get = await app.request("/v1/names", { + headers: { Origin: "https://enspack.dev" }, + }); + expect(get.status).toBe(200); + expect(get.headers.get("access-control-allow-origin")).toBe("*"); + + const preflight = await app.request("/v1/names", { + method: "OPTIONS", + headers: { + Origin: "https://enspack.dev", + "Access-Control-Request-Method": "GET", + }, + }); + expect([200, 204]).toContain(preflight.status); + }); }); From 69dfec47afcb509258aeb1da1b781697765f6e29 Mon Sep 17 00:00:00 2001 From: Jeff Lau Date: Mon, 14 Sep 2026 16:47:34 +0000 Subject: [PATCH 3/9] feat(site): layout chrome, shared CSS primitives, docs Co-authored-by: Jeff Lau --- README.md | 1 + apps/site/CHANGELOG.md | 5 + apps/site/README.md | 50 +++++ apps/site/src/components/copy-button.tsx | 1 + apps/site/src/components/layout.tsx | 82 +++++++-- apps/site/src/styles/global.css | 222 ++++++++++++++++++++++- apps/site/src/styles/tokens.css | 1 + apps/site/test/layout.test.tsx | 69 +++++++ 8 files changed, 415 insertions(+), 16 deletions(-) create mode 100644 apps/site/CHANGELOG.md create mode 100644 apps/site/README.md create mode 100644 apps/site/test/layout.test.tsx diff --git a/README.md b/README.md index dbedd5c..9de0c3d 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ not published yet (MVP.md §0.1). Local proof uses Anvil + `test/fixtures/tiny-m | `services/seed` | `@enspack/seed` | seed node API + qBittorrent + Kubo compose | | `services/registrar` | `@enspack/registrar` | HF-proof → `enspack.eth` publisher subnames | | `services/indexer` | `@enspack/indexer` | Ponder indexer + JSON API | +| `apps/site` | `@enspack/site` | catalog site, read-only view of the indexer API | | `bootstrap/` | `@enspack/bootstrap` | `models.yaml` mirror runner (`enspack-bootstrap plan|run`) | | `infra/` | `@enspack/infra` | one-box Sepolia deploy (Caddy + compose + scripts) | | `test/fixtures` | — | tiny-model folder + torrent for swarm tests | diff --git a/apps/site/CHANGELOG.md b/apps/site/CHANGELOG.md new file mode 100644 index 0000000..8fd7802 --- /dev/null +++ b/apps/site/CHANGELOG.md @@ -0,0 +1,5 @@ +# @enspack/site + +## 0.1.0 (Unreleased) + +- Initial catalog site (WP-19): read-only view of the indexer JSON API. diff --git a/apps/site/README.md b/apps/site/README.md new file mode 100644 index 0000000..c51d395 --- /dev/null +++ b/apps/site/README.md @@ -0,0 +1,50 @@ +# @enspack/site + +Read-only catalog for enspack names. Someone who has never heard of enspack +should see, on one page, what a name is, what it points at, and that the bytes +verify. The site is a window onto the indexer JSON API (`MVP.md` §4.3). No +wallet, no writes, no keys, no RPC URL in the bundle. + +`@enspack/core` is a type-only (dev) dependency. Runtime core is never imported +into the browser bundle (it would pull viem and IPFS). + +## Environment + +| Variable | Default | Meaning | +| --- | --- | --- | +| `VITE_INDEX_URL` | `https://index.enspack.dev` | Indexer base URL | +| `VITE_REGISTRAR_URL` | `https://registrar.enspack.dev` | Registrar base URL (attestations, claim-page link) | +| `VITE_CHAIN` | `sepolia` | `sepolia` \| `mainnet`; drives the testnet banner and explorer / ENS-app links | +| `VITE_DATA_SOURCE` | `http` | `http` \| `demo`; `demo` serves the recorded fixtures in-app | + +## Develop + +From the repo root: + +```sh +pnpm --filter @enspack/site dev +``` + +Without a live indexer, serve the recorded fixtures: + +```sh +VITE_DATA_SOURCE=demo pnpm --filter @enspack/site dev +``` + +```sh +pnpm --filter @enspack/site build +pnpm --filter @enspack/site preview +``` + +## Deploy (Cloudflare Pages) + +Hosting target is `enspack.dev`. DNS and the Pages project are manual. + +| | | +| --- | --- | +| Build command | `pnpm --filter @enspack/site build` | +| Output directory | `apps/site/dist` | +| SPA fallback | `public/_redirects` (`/* /index.html 200`) | + +The built bundle contains no keys and no RPC URLs. Chain state arrives only +through the indexer (or demo fixtures). diff --git a/apps/site/src/components/copy-button.tsx b/apps/site/src/components/copy-button.tsx index 175eb55..89cae41 100644 --- a/apps/site/src/components/copy-button.tsx +++ b/apps/site/src/components/copy-button.tsx @@ -23,6 +23,7 @@ export function CopyButton({ value, label = "Copy" }: { value: string; label?: s type="button" className="copy-btn" onClick={onClick} + aria-live="polite" aria-label={copied ? "Copied" : `${label} to clipboard`} data-copied={copied ? "true" : undefined} > diff --git a/apps/site/src/components/layout.tsx b/apps/site/src/components/layout.tsx index 20b78fd..113f0d5 100644 --- a/apps/site/src/components/layout.tsx +++ b/apps/site/src/components/layout.tsx @@ -1,38 +1,90 @@ -import type { ReactNode } from "react"; +import { type ReactNode, useState } from "react"; import { Link, NavLink } from "react-router"; import { config } from "../lib/config.js"; +import { ExternalLink } from "./external-link.js"; + +/** Session-only so a new tab still sees the banner; reload in this tab stays dismissed. */ +const BANNER_STORAGE_KEY = "enspack.banner.dismissed"; + +function bannerDismissed(): boolean { + try { + return sessionStorage.getItem(BANNER_STORAGE_KEY) === "1"; + } catch { + return false; + } +} + +function persistBannerDismissed(): void { + try { + sessionStorage.setItem(BANNER_STORAGE_KEY, "1"); + } catch { + // private mode can throw; still hide for this document + } +} + +function WordmarkMark() { + return ( + + ); +} export function Layout({ children }: { children: ReactNode }) { + const [showBanner, setShowBanner] = useState( + () => config.chain !== "mainnet" && !bannerDismissed(), + ); + + function dismissBanner(): void { + persistBannerDismissed(); + setShowBanner(false); + } + return (
- {config.chain !== "mainnet" && ( -
- Showing {config.chain} testnet data. Mainnet follows once the v1 path is live. -
+ + Skip to content + + {showBanner && ( + )}
+ enspack
-
{children}
+
+ {children} +
enspack/0.1 · {config.chain} - - - SPEC - {" "} - · index API + + SPEC.md + {" · "} + {config.indexUrl} + {" · "} + registrar
diff --git a/apps/site/src/styles/global.css b/apps/site/src/styles/global.css index badedfe..8d05178 100644 --- a/apps/site/src/styles/global.css +++ b/apps/site/src/styles/global.css @@ -56,7 +56,31 @@ button { flex-direction: column; } +.skip-link { + position: absolute; + left: var(--sp-4); + top: var(--sp-2); + z-index: 100; + transform: translateY(-200%); + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--sp-2) var(--sp-3); + font-size: var(--fs-sm); +} + +.skip-link:focus { + transform: none; + text-decoration: none; +} + .banner { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: var(--sp-3); background: var(--warn-soft); color: var(--text); border-bottom: 1px solid var(--border); @@ -65,11 +89,32 @@ button { padding: var(--sp-2) var(--sp-4); } +.banner-text { + margin: 0; +} + +.banner-dismiss { + border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius); + padding: 2px var(--sp-2); + font-size: var(--fs-xs); + line-height: 1.4; + cursor: pointer; + color: var(--muted); +} + +.banner-dismiss:hover { + color: var(--text); + border-color: var(--muted); +} + .site-header { display: flex; align-items: center; justify-content: space-between; - gap: var(--sp-6); + flex-wrap: wrap; + gap: var(--sp-4) var(--sp-6); max-width: var(--content-max); width: 100%; margin: 0 auto; @@ -77,20 +122,39 @@ button { } .wordmark { + display: inline-flex; + align-items: center; + gap: var(--sp-2); font-family: var(--font-mono); font-weight: 600; font-size: var(--fs-lg); color: var(--text); + letter-spacing: -0.02em; +} + +.wordmark:hover { + color: var(--text); + text-decoration: none; +} + +.wordmark-mark { + display: block; + flex-shrink: 0; + color: var(--accent); } .site-nav { display: flex; + flex-wrap: wrap; + align-items: center; gap: var(--sp-5); font-size: var(--fs-sm); } .site-nav a { color: var(--muted); + padding-block: var(--sp-1); + border-bottom: 2px solid transparent; } .site-nav a.active, @@ -99,6 +163,21 @@ button { text-decoration: none; } +.site-nav a.active { + border-bottom-color: var(--accent); +} + +@media (max-width: 640px) { + .site-header { + align-items: flex-start; + } + + .site-nav { + flex-basis: 100%; + gap: var(--sp-3) var(--sp-4); + } +} + .site-main { flex: 1; width: 100%; @@ -113,6 +192,7 @@ button { font-size: var(--fs-xs); display: flex; justify-content: space-between; + flex-wrap: wrap; gap: var(--sp-4); max-width: var(--content-max); width: 100%; @@ -120,6 +200,143 @@ button { padding: var(--sp-4) var(--sp-6); } +.site-footer a { + color: var(--muted); +} + +.site-footer a:hover { + color: var(--text); +} + +.site-footer-links { + overflow-wrap: anywhere; +} + +/* Pages */ +.page { + display: flex; + flex-direction: column; + gap: var(--sp-8); +} + +.page-title { + font-size: var(--fs-2xl); + font-weight: 600; + letter-spacing: -0.03em; + line-height: 1.15; + margin: 0 0 var(--sp-3); +} + +.section { + display: flex; + flex-direction: column; + gap: var(--sp-4); +} + +.section-title { + font-size: var(--fs-lg); + font-weight: 600; + margin: 0; +} + +.grid-2 { + display: grid; + grid-template-columns: minmax(0, 1fr) var(--aside); + gap: var(--sp-8); + align-items: start; +} + +@media (max-width: 960px) { + .grid-2 { + grid-template-columns: minmax(0, 1fr); + } +} + +/* Tables */ +.table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.table th, +.table td { + text-align: left; + padding: var(--sp-2) var(--sp-3); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.table th { + color: var(--muted); + font-weight: 600; + font-size: var(--fs-xs); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.table td { + overflow-wrap: anywhere; +} + +.table-sticky { + overflow: auto; +} + +.table-sticky thead th { + position: sticky; + top: 0; + background: var(--bg); + z-index: 1; + box-shadow: 0 1px 0 var(--border); +} + +/* Key / value */ +.kv { + display: grid; + grid-template-columns: minmax(7rem, 12rem) minmax(0, 1fr); + gap: var(--sp-2) var(--sp-4); + font-size: var(--fs-sm); + margin: 0; +} + +.kv dt { + color: var(--muted); + font-weight: 500; +} + +.kv dd { + margin: 0; + overflow-wrap: anywhere; +} + +/* Command block: room for a copy button in the top-right */ +.cmd { + position: relative; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-4); + padding-right: 5.5rem; + overflow-x: auto; + font-family: var(--font-mono); + font-size: var(--fs-sm); + margin: 0; +} + +.cmd pre { + margin: 0; + font-family: inherit; + font-size: inherit; + overflow-x: auto; +} + +.cmd .copy-btn { + position: absolute; + top: var(--sp-2); + right: var(--sp-2); +} + /* Primitives */ .copy-btn { border: 1px solid var(--border); @@ -131,6 +348,7 @@ button { cursor: pointer; color: var(--muted); white-space: nowrap; + flex-shrink: 0; } .copy-btn:hover { @@ -146,6 +364,7 @@ button { .hash { display: inline-flex; align-items: baseline; + flex-wrap: wrap; gap: var(--sp-2); max-width: 100%; } @@ -212,6 +431,7 @@ button { .state-error { border-color: var(--danger); + border-style: solid; } .card { diff --git a/apps/site/src/styles/tokens.css b/apps/site/src/styles/tokens.css index 5f2842c..ff1c41f 100644 --- a/apps/site/src/styles/tokens.css +++ b/apps/site/src/styles/tokens.css @@ -41,6 +41,7 @@ --sp-16: 64px; --content-max: 1120px; + --aside: 340px; } @media (prefers-color-scheme: light) { diff --git a/apps/site/test/layout.test.tsx b/apps/site/test/layout.test.tsx new file mode 100644 index 0000000..c009ff4 --- /dev/null +++ b/apps/site/test/layout.test.tsx @@ -0,0 +1,69 @@ +import { cleanup, fireEvent, render, screen, within } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { MemoryRouter } from "react-router"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { Layout } from "../src/components/layout.js"; +import { ClientProvider } from "../src/lib/client-context.js"; +import { fixtureClient } from "./helpers.js"; + +function renderLayout(ui: ReactNode =

page

) { + return render( + + + {ui} + + , + ); +} + +describe("Layout", () => { + beforeEach(() => { + sessionStorage.clear(); + }); + + afterEach(() => { + cleanup(); + sessionStorage.clear(); + }); + + it("renders the Sepolia banner and hides it after dismiss", () => { + const { unmount } = renderLayout(); + expect( + screen.getByText("Showing Sepolia testnet data. Mainnet follows once the v1 path is live."), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "Dismiss" })); + expect( + screen.queryByText("Showing Sepolia testnet data. Mainnet follows once the v1 path is live."), + ).not.toBeInTheDocument(); + + unmount(); + renderLayout(); + expect( + screen.queryByText("Showing Sepolia testnet data. Mainnet follows once the v1 path is live."), + ).not.toBeInTheDocument(); + }); + + it("nav links have the catalog hrefs", () => { + renderLayout(); + const nav = screen.getByRole("navigation", { name: "Main" }); + expect(within(nav).getByRole("link", { name: "Models" })).toHaveAttribute("href", "/"); + expect(within(nav).getByRole("link", { name: "Violations" })).toHaveAttribute( + "href", + "/violations", + ); + expect(within(nav).getByRole("link", { name: "Get started" })).toHaveAttribute( + "href", + "/get-started", + ); + expect(within(nav).getByRole("link", { name: "GitHub" })).toHaveAttribute( + "href", + "https://github.com/jefflau/enspack", + ); + }); + + it("footer shows enspack/0.1", () => { + renderLayout(); + expect(screen.getByText("enspack/0.1")).toBeInTheDocument(); + }); +}); From cbe7514aba6932eae96ad9daaf9021262a3aaf5e Mon Sep 17 00:00:00 2001 From: Jeff Lau Date: Mon, 14 Sep 2026 16:47:34 +0000 Subject: [PATCH 4/9] feat(site): home, model, publisher, violations and get-started pages Model page renders the trust chain (ENS name -> CID -> manifest -> bytes -> per-file SHA-256), a sortable files table, versions and a get-it panel with the lockfile entry. Home has live stats, how-it-works and a searchable list. Co-authored-by: Jeff Lau --- apps/site/src/components/command-block.tsx | 13 + apps/site/src/components/files-table.tsx | 100 +++++ apps/site/src/components/get-panel.tsx | 65 ++++ apps/site/src/components/hero.tsx | 23 ++ apps/site/src/components/how-it-works.tsx | 41 ++ apps/site/src/components/model-list.tsx | 147 +++++++ apps/site/src/components/stats.tsx | 28 ++ apps/site/src/components/trust-chain.tsx | 175 +++++++++ apps/site/src/components/versions-list.tsx | 72 ++++ apps/site/src/pages/get-started.tsx | 105 ++++- apps/site/src/pages/home.tsx | 108 +++++- apps/site/src/pages/model.tsx | 113 +++++- apps/site/src/pages/not-found.tsx | 9 +- apps/site/src/pages/publisher.tsx | 206 +++++++++- apps/site/src/pages/violations.tsx | 85 +++- apps/site/src/styles/pages/get-started.css | 83 ++++ apps/site/src/styles/pages/home.css | 287 ++++++++++++++ apps/site/src/styles/pages/model.css | 431 +++++++++++++++++++++ apps/site/src/styles/pages/publisher.css | 116 ++++++ apps/site/src/styles/pages/violations.css | 62 +++ apps/site/test/get-started.test.tsx | 17 + apps/site/test/home.test.tsx | 74 ++++ apps/site/test/model.test.tsx | 131 +++++++ apps/site/test/publisher.test.tsx | 43 ++ apps/site/test/violations.test.tsx | 35 ++ 25 files changed, 2563 insertions(+), 6 deletions(-) create mode 100644 apps/site/src/components/command-block.tsx create mode 100644 apps/site/src/components/files-table.tsx create mode 100644 apps/site/src/components/get-panel.tsx create mode 100644 apps/site/src/components/hero.tsx create mode 100644 apps/site/src/components/how-it-works.tsx create mode 100644 apps/site/src/components/model-list.tsx create mode 100644 apps/site/src/components/stats.tsx create mode 100644 apps/site/src/components/trust-chain.tsx create mode 100644 apps/site/src/components/versions-list.tsx create mode 100644 apps/site/src/styles/pages/get-started.css create mode 100644 apps/site/src/styles/pages/home.css create mode 100644 apps/site/src/styles/pages/model.css create mode 100644 apps/site/src/styles/pages/publisher.css create mode 100644 apps/site/src/styles/pages/violations.css create mode 100644 apps/site/test/get-started.test.tsx create mode 100644 apps/site/test/home.test.tsx create mode 100644 apps/site/test/model.test.tsx create mode 100644 apps/site/test/publisher.test.tsx create mode 100644 apps/site/test/violations.test.tsx diff --git a/apps/site/src/components/command-block.tsx b/apps/site/src/components/command-block.tsx new file mode 100644 index 0000000..4cc905f --- /dev/null +++ b/apps/site/src/components/command-block.tsx @@ -0,0 +1,13 @@ +import { CopyButton } from "./copy-button.js"; + +/** Copyable `
` for CLI snippets. Styled by the importing page (get-started.css). */
+export function CommandBlock({ command }: { command: string }) {
+  return (
+    
+
+        {command}
+      
+ +
+ ); +} diff --git a/apps/site/src/components/files-table.tsx b/apps/site/src/components/files-table.tsx new file mode 100644 index 0000000..60c6f2f --- /dev/null +++ b/apps/site/src/components/files-table.tsx @@ -0,0 +1,100 @@ +import { useMemo, useState } from "react"; +import type { ManifestFile } from "../lib/api.js"; +import { formatBytes } from "../lib/format.js"; +import { RoleBadge } from "./badge.js"; +import { Hash } from "./hash.js"; + +type SortKey = "path" | "size"; +type SortDir = "asc" | "desc"; + +function ariaSort( + active: SortKey | null, + key: SortKey, + dir: SortDir, +): "ascending" | "descending" | "none" { + if (active !== key) return "none"; + return dir === "asc" ? "ascending" : "descending"; +} + +function compareFiles(a: ManifestFile, b: ManifestFile, key: SortKey, dir: SortDir): number { + const mul = dir === "asc" ? 1 : -1; + if (key === "size") return (a.size - b.size) * mul; + return a.path.localeCompare(b.path) * mul; +} + +export function FilesTable({ + files, + totalSize, +}: { + files: readonly ManifestFile[]; + totalSize: number; +}) { + const [key, setKey] = useState(null); + const [dir, setDir] = useState("asc"); + + const rows = useMemo(() => { + if (key === null) return [...files]; + return [...files].sort((a, b) => compareFiles(a, b, key, dir)); + }, [files, key, dir]); + + function onSort(next: SortKey) { + if (key === next) { + setDir((d) => (d === "asc" ? "desc" : "asc")); + return; + } + setKey(next); + setDir("asc"); + } + + return ( +
+

Files

+
+ + + + + + + + + + + {rows.map((file) => ( + + + + + + + ))} + + + + + + + +
+ + role + + sha256
+ {file.path} + + + {formatBytes(file.size)} + +
+ {files.length} {files.length === 1 ? "file" : "files"} + + {formatBytes(totalSize)} +
+
+
+ ); +} diff --git a/apps/site/src/components/get-panel.tsx b/apps/site/src/components/get-panel.tsx new file mode 100644 index 0000000..118c4a6 --- /dev/null +++ b/apps/site/src/components/get-panel.tsx @@ -0,0 +1,65 @@ +import type { Manifest } from "../lib/api.js"; +import { CopyButton } from "./copy-button.js"; + +function lockEntryJson(manifest: Manifest, cid: string): string { + const entry: { + resolved: string; + cid: string; + infohash: string; + totalSize: number; + select: string[]; + upstream?: { repo: string; revision: string }; + } = { + resolved: manifest.name, + cid, + infohash: manifest.distribution.infohash, + totalSize: manifest.totalSize, + select: ["*"], + }; + if (manifest.upstream !== undefined) { + entry.upstream = { + repo: manifest.upstream.repo, + revision: manifest.upstream.revision, + }; + } + return JSON.stringify( + { + lockfileVersion: 1, + models: { + [manifest.model]: entry, + }, + }, + null, + 2, + ); +} + +function CopyBlock({ label, text }: { label: string; text: string }) { + return ( +
+
+ {label} + +
+
{text}
+
+ ); +} + +export function GetPanel({ manifest, cid }: { manifest: Manifest; cid: string }) { + const getCmd = `enspack get ${manifest.model}`; + const addCmd = `enspack add ${manifest.model}\nenspack install`; + const lockJson = lockEntryJson(manifest, cid); + + return ( + + ); +} diff --git a/apps/site/src/components/hero.tsx b/apps/site/src/components/hero.tsx new file mode 100644 index 0000000..861eb1f --- /dev/null +++ b/apps/site/src/components/hero.tsx @@ -0,0 +1,23 @@ +import { CopyButton } from "./copy-button.js"; + +const QWEN_EXAMPLE = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; + +export function Hero({ firstModel }: { firstModel?: string }) { + const name = firstModel ?? QWEN_EXAMPLE; + const command = `enspack get ${name}`; + return ( +
+

Model weights with a name you can verify.

+

+ An ENS name points at a content-addressed manifest; every file is SHA-256 checked; bytes + come from BitTorrent + HF webseeds, never trusted. +

+
+
+          {command}
+        
+ +
+
+ ); +} diff --git a/apps/site/src/components/how-it-works.tsx b/apps/site/src/components/how-it-works.tsx new file mode 100644 index 0000000..9ff27ce --- /dev/null +++ b/apps/site/src/components/how-it-works.tsx @@ -0,0 +1,41 @@ +const STEPS = [ + { + title: "Name", + artifact: "v1-0-0.qwen--….mirrors.enspack.eth", + trust: "The owner key of this name is the only party that can point it at a manifest.", + }, + { + title: "Manifest", + artifact: "contenthash → bafy…", + trust: "The CID is the hash of the JSON; a gateway cannot change it.", + }, + { + title: "Bytes", + artifact: "magnet:?xt=urn:btih:… + webseeds", + trust: "Peers and HF webseeds serve files; none is a source of truth.", + }, + { + title: "Verify", + artifact: "sha256 per file → .enspack-quarantine/", + trust: "Every file is checked by size then SHA-256; a mismatch is quarantined.", + }, +] as const; + +export function HowItWorks() { + return ( +
+

+ How it works +

+
    + {STEPS.map((step) => ( +
  1. +

    {step.title}

    + {step.artifact} +

    {step.trust}

    +
  2. + ))} +
+
+ ); +} diff --git a/apps/site/src/components/model-list.tsx b/apps/site/src/components/model-list.tsx new file mode 100644 index 0000000..3c852c3 --- /dev/null +++ b/apps/site/src/components/model-list.tsx @@ -0,0 +1,147 @@ +import { Link } from "react-router"; +import type { NameListItem } from "../lib/api.js"; +import type { Chain } from "../lib/config.js"; +import { firstLabel, formatBytes } from "../lib/format.js"; +import { LicenseBadge } from "./badge.js"; +import { Hash } from "./hash.js"; +import { EmptyState, ErrorState, Loading } from "./states.js"; + +function displayFromModel(model: string): string { + return firstLabel(model).replaceAll("--", "/"); +} + +export function ModelList({ + items, + nextCursor, + loading, + loadingMore, + error, + qInput, + onQChange, + publishers, + selectedPublisher, + onPublisherChange, + onLoadMore, + chain, +}: { + items: NameListItem[]; + nextCursor: string | null; + loading: boolean; + loadingMore: boolean; + error: Error | null; + qInput: string; + onQChange: (value: string) => void; + publishers: { name: string }[]; + selectedPublisher: string | null; + onPublisherChange: (name: string | null) => void; + onLoadMore: () => void; + chain: Chain; +}) { + const filtered = qInput !== "" || selectedPublisher !== null; + + return ( +
+

+ Models +

+
+ +
+ Filter by publisher + + {publishers.map((p) => ( + + ))} +
+
+ {loading ? ( + + ) : error ? ( + + ) : items.length === 0 ? ( + + {filtered ? null : ( +

+ Publish one: enspack publish --from-hf <org/repo>. +

+ )} +
+ ) : ( + <> +
+ + + + + + + + + + + + {items.map((item) => ( + + + + + + + + ))} + +
NamePublisherLicenseSizeLatest
+ + {displayFromModel(item.model)} + + + + + {item.publisher} + + + + {formatBytes(item.totalSize)}{item.latest.version}
+
+ {nextCursor !== null && ( + + )} + + )} +
+ ); +} diff --git a/apps/site/src/components/stats.tsx b/apps/site/src/components/stats.tsx new file mode 100644 index 0000000..0e0ffdd --- /dev/null +++ b/apps/site/src/components/stats.tsx @@ -0,0 +1,28 @@ +import { formatBytes } from "../lib/format.js"; + +export function Stats({ + models, + publishers, + bytes, +}: { + models: number; + publishers: number; + bytes: number; +}) { + return ( +
+
+
{models}
+
models
+
+
+
{publishers}
+
publishers
+
+
+
{formatBytes(bytes)}
+
verified bytes
+
+
+ ); +} diff --git a/apps/site/src/components/trust-chain.tsx b/apps/site/src/components/trust-chain.tsx new file mode 100644 index 0000000..bf5159f --- /dev/null +++ b/apps/site/src/components/trust-chain.tsx @@ -0,0 +1,175 @@ +import type { ReactNode } from "react"; +import type { Manifest } from "../lib/api.js"; +import type { Chain } from "../lib/config.js"; +import { ensAppUrl, formatBytes, formatDate, ipfsUrl } from "../lib/format.js"; +import { ExternalLink } from "./external-link.js"; +import { Hash } from "./hash.js"; + +function BytesField({ label, children }: { label: string; children: ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +export function TrustChain({ + manifest, + cid, + chain, +}: { + manifest: Manifest; + cid: string; + chain: Chain; +}) { + const dist = manifest.distribution; + const torrent = dist.torrent; + const torrentCid = torrent?.cid; + const torrentUrl = torrent?.url; + const seeds = dist.seeds; + const ipfs = dist.ipfs; + const oci = dist.oci; + const hb = dist.hb; + + return ( +
+

Trust chain

+
    +
  1. +
  2. + +
  3. +
  4. + +
  5. +
  6. + +
  7. +
  8. + +
  9. +
  10. +
+
+ ); +} diff --git a/apps/site/src/components/versions-list.tsx b/apps/site/src/components/versions-list.tsx new file mode 100644 index 0000000..a807c95 --- /dev/null +++ b/apps/site/src/components/versions-list.tsx @@ -0,0 +1,72 @@ +import { Link } from "react-router"; +import type { NameVersion } from "../lib/api.js"; +import { formatDate } from "../lib/format.js"; +import { Badge } from "./badge.js"; +import { CopyButton } from "./copy-button.js"; +import { Hash } from "./hash.js"; + +/** Hash + in-app link without nesting a copy button inside the ``. */ +function VersionName({ name }: { name: string }) { + return ( + + + {name} + + + + ); +} + +export function VersionsList({ + versions, + latestName, +}: { + versions: readonly NameVersion[]; + latestName: string; +}) { + return ( +
+

Versions

+
+ + + + + + + + + + + {versions.map((v) => { + const latest = v.name === latestName; + return ( + + + + + + + ); + })} + +
versionnamecidcreatedAt
+ {v.version} + {latest ? latest : null} + + + + + {formatDate(v.createdAt)}
+
+

+ Version names are immutable by convention; a changed contenthash on one appears under{" "} + Violations. +

+
+ ); +} diff --git a/apps/site/src/pages/get-started.tsx b/apps/site/src/pages/get-started.tsx index 15e4364..01c4d1f 100644 --- a/apps/site/src/pages/get-started.tsx +++ b/apps/site/src/pages/get-started.tsx @@ -1,3 +1,106 @@ +import { CommandBlock } from "../components/command-block.js"; +import { ExternalLink } from "../components/external-link.js"; +import { config } from "../lib/config.js"; +import "../styles/pages/get-started.css"; + +const EXAMPLE = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; +const GET_CMD = `enspack get ${EXAMPLE}`; +const ADD_CMD = `enspack add ${EXAMPLE}`; +const PUBLISH_CMD = "enspack publish --from-hf --publisher --version "; + export function GetStartedPage() { - return
GetStartedPage
; + const claimHref = `${config.registrarUrl}/`; + const specHref = `${config.repoUrl}/blob/master/SPEC.md`; + + return ( +
+

Get started

+

+ An ENS name points at a content-addressed manifest. The CLI checks every file by SHA-256 and + fetches bytes from BitTorrent plus HF webseeds — hosts are never trusted. +

+ +
+

Install

+

+ Requires Node 22+ and aria2c on PATH (the downloader the CLI + shells out to). +

+ +
+ +
+

Fetch a model

+

+ Resolves the name, downloads the manifest CID, then the torrent + webseeds, and verifies + each file before writing the HF cache. +

+ + {config.chain !== "mainnet" && ( +

+ This catalog is on the {config.chain} testnet. Pass --chain {config.chain}{" "} + so the CLI reads the same names, for example{" "} + + {GET_CMD} --chain {config.chain} + + . +

+ )} +
+ +
+

Pin in a project

+

+ enspack add resolves a name and appends it to enspack.lock.{" "} + enspack install reads the lockfile, requires each CID to match, then + downloads and verifies. A CID mismatch exits 3 (unless you pass --update). +

+ + +
+ +
+

Publish

+

+ Build a manifest from a Hugging Face repo and write the version + model names on-chain. + ENSv1 (SPEC §8) is 3 transactions for a new model and 2 for a new version. ENSv2 + (Sepolia's default) differs: 4 txs for a new model and 2 for a new version, plus + first-time publisher setup. +

+ +
+ +
+

Claim a publisher name

+

+ Proof of Hugging Face org or user control binds a label under enspack.eth to + your wallet (SPEC §7): +

+
    +
  • + Request a claim: POST /v1/claims with your HF namespace and address; you + get a claimId and a challenge. +
  • +
  • + Commit enspack-verify.txt to a public repo under that namespace, containing + exactly the challenge and address. +
  • +
  • + Sign the challenge with the same address (EIP-191 personal_sign). +
  • +
  • + Submit POST /v1/claims/{"{claimId}"}/verify with the repo and signature. + The registrar issues the name and stores the attestation permanently. +
  • +
+ + Claim <you>.enspack.eth + +

+ SPEC.md + GitHub +

+
+
+ ); } diff --git a/apps/site/src/pages/home.tsx b/apps/site/src/pages/home.tsx index 068d0bc..8303bf9 100644 --- a/apps/site/src/pages/home.tsx +++ b/apps/site/src/pages/home.tsx @@ -1,3 +1,109 @@ +import { useEffect, useState } from "react"; +import { Hero } from "../components/hero.js"; +import { HowItWorks } from "../components/how-it-works.js"; +import { ModelList } from "../components/model-list.js"; +import { Stats } from "../components/stats.js"; +import type { ListNamesParams, NameListItem } from "../lib/api.js"; +import { useClient } from "../lib/client-context.js"; +import { config } from "../lib/config.js"; +import { useQuery } from "../lib/use-query.js"; +import "../styles/pages/home.css"; + +const SEARCH_DEBOUNCE_MS = 250; + +function namesParams(q: string, publisher: string | null, cursor?: string): ListNamesParams { + const params: ListNamesParams = {}; + if (q) params.q = q; + if (publisher) params.publisher = publisher; + if (cursor) params.cursor = cursor; + return params; +} + export function HomePage() { - return
HomePage
; + const client = useClient(); + const [qInput, setQInput] = useState(""); + const [q, setQ] = useState(""); + const [publisher, setPublisher] = useState(null); + const [items, setItems] = useState([]); + const [nextCursor, setNextCursor] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [namesError, setNamesError] = useState(null); + + const publishers = useQuery("publishers", () => client.listPublishers()); + + useEffect(() => { + if (qInput === q) return; + const t = setTimeout(() => setQ(qInput), SEARCH_DEBOUNCE_MS); + return () => clearTimeout(t); + }, [qInput, q]); + + // useQuery resets on key change, which would drop already-loaded pages. + useEffect(() => { + let cancelled = false; + setLoading(true); + setNamesError(null); + client.listNames(namesParams(q, publisher)).then( + (page) => { + if (cancelled) return; + setItems(page.items); + setNextCursor(page.nextCursor); + setLoading(false); + }, + (err: unknown) => { + if (cancelled) return; + setItems([]); + setNextCursor(null); + setNamesError(err instanceof Error ? err : new Error(String(err))); + setLoading(false); + }, + ); + return () => { + cancelled = true; + }; + }, [client, q, publisher]); + + function loadMore() { + if (nextCursor === null || loadingMore) return; + setLoadingMore(true); + client.listNames(namesParams(q, publisher, nextCursor)).then( + (page) => { + setItems((prev) => [...prev, ...page.items]); + setNextCursor(page.nextCursor); + setLoadingMore(false); + }, + (err: unknown) => { + setNamesError(err instanceof Error ? err : new Error(String(err))); + setLoadingMore(false); + }, + ); + } + + const first = items[0]; + const bytes = items.reduce((sum, item) => sum + item.totalSize, 0); + const publisherCount = publishers.data?.items.length ?? 0; + + return ( +
+ {first ? : } + {!loading && namesError === null && ( + + )} + + +
+ ); } diff --git a/apps/site/src/pages/model.tsx b/apps/site/src/pages/model.tsx index bcce01f..0bb3fb6 100644 --- a/apps/site/src/pages/model.tsx +++ b/apps/site/src/pages/model.tsx @@ -1,3 +1,114 @@ +import { Link, useParams } from "react-router"; +import { LicenseBadge } from "../components/badge.js"; +import { ExternalLink } from "../components/external-link.js"; +import { FilesTable } from "../components/files-table.js"; +import { GetPanel } from "../components/get-panel.js"; +import { Hash } from "../components/hash.js"; +import { EmptyState, ErrorState, Loading } from "../components/states.js"; +import { TrustChain } from "../components/trust-chain.js"; +import { VersionsList } from "../components/versions-list.js"; +import { ApiError, type NameDetail, type NameVersion } from "../lib/api.js"; +import { useClient } from "../lib/client-context.js"; +import { config } from "../lib/config.js"; +import { ensAppUrl, formatBytes, shortHex } from "../lib/format.js"; +import { useQuery } from "../lib/use-query.js"; +import "../styles/pages/model.css"; + +function decodeNameParam(raw: string | undefined): string | null { + if (raw === undefined || raw === "") return null; + try { + return decodeURIComponent(raw); + } catch { + return null; + } +} + +/** Latest CID: the version whose name matches the manifest, else the last listed. */ +function latestVersion(detail: NameDetail): NameVersion { + const match = detail.versions.find((v) => v.name === detail.manifest.name); + if (match !== undefined) return match; + const last = detail.versions[detail.versions.length - 1]; + if (last !== undefined) return last; + return detail.manifest.versions[0]; +} + +function ModelHeader({ detail }: { detail: NameDetail }) { + const { manifest } = detail; + const title = manifest.displayName ?? manifest.model; + const upstream = manifest.upstream; + const canonical = manifest.canonical; + + return ( +
+

{title}

+ +

+ + {manifest.publisher} + + + {formatBytes(manifest.totalSize)} + {upstream !== undefined && ( + + {upstream.url} @{shortHex(upstream.revision)} + + )} +

+ {canonical !== undefined && ( +

+ Publisher-verified name:{" "} + +

+ )} +
+ ); +} + export function ModelPage() { - return
ModelPage
; + const { name: raw } = useParams(); + const name = decodeNameParam(raw); + const client = useClient(); + const { data, error, loading } = useQuery(`name:${name ?? ""}`, () => { + if (name === null) { + return Promise.reject(new ApiError(404, "NOT_FOUND", "unknown name")); + } + return client.getName(name); + }); + + if (loading) return ; + if (error instanceof ApiError && error.status === 404) { + return ( + + Home + + ); + } + if (error) return ; + if (data === null) return ; + + const latest = latestVersion(data); + + return ( +
+

+ Indexed from {config.chain}. Verification shown here is the indexer's; the CLI + re-verifies every file locally. +

+ +
+
+ + + +
+ +
+
+ ); } diff --git a/apps/site/src/pages/not-found.tsx b/apps/site/src/pages/not-found.tsx index 4bfd14d..03c25b1 100644 --- a/apps/site/src/pages/not-found.tsx +++ b/apps/site/src/pages/not-found.tsx @@ -1,3 +1,10 @@ +import { Link } from "react-router"; +import { EmptyState } from "../components/states.js"; + export function NotFoundPage() { - return
NotFoundPage
; + return ( + + Back to models + + ); } diff --git a/apps/site/src/pages/publisher.tsx b/apps/site/src/pages/publisher.tsx index b00b8fa..2e8743a 100644 --- a/apps/site/src/pages/publisher.tsx +++ b/apps/site/src/pages/publisher.tsx @@ -1,3 +1,207 @@ +import { Link, useParams } from "react-router"; +import { Badge, LicenseBadge } from "../components/badge.js"; +import { CopyButton } from "../components/copy-button.js"; +import { ExternalLink } from "../components/external-link.js"; +import { Hash } from "../components/hash.js"; +import { EmptyState, ErrorState, Loading } from "../components/states.js"; +import type { Attestation, NameListItem } from "../lib/api.js"; +import { useClient } from "../lib/client-context.js"; +import type { IndexClient } from "../lib/client.js"; +import { config } from "../lib/config.js"; +import { explorerTx, firstLabel, formatBytes, hfUrl, isAddress, isTxHash } from "../lib/format.js"; +import { useQuery } from "../lib/use-query.js"; +import "../styles/pages/publisher.css"; + +function decodePublisherParam(raw: string | undefined): string | null { + if (raw === undefined || raw === "") return null; + try { + return decodeURIComponent(raw); + } catch { + return null; + } +} + +async function loadPublisherPage(client: IndexClient, name: string) { + const [pubs, namesPage, attestation] = await Promise.all([ + client.listPublishers(), + client.listNames({ publisher: name }), + // Spec §4.3: missing or unreachable attestation is absence, not a page error. + client + .getAttestation(firstLabel(name)) + .catch(() => null), + ]); + return { + publisher: pubs.items.find((p) => p.name === name) ?? null, + models: namesPage.items, + attestation, + }; +} + +function attestationItemKey(item: unknown, i: number): string { + if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") { + return String(item); + } + return String(i); +} + +function AttestationValue({ value }: { value: unknown }) { + if (value === null || value === undefined) { + return ; + } + if (isTxHash(value)) { + return ( + + {value} + + ); + } + if (isAddress(value)) { + return ; + } + if (Array.isArray(value)) { + if (value.length === 0) return []; + return ( +
+ {value.map((item, i) => ( +
+ +
+ ))} +
+ ); + } + if (typeof value === "object") { + return
{JSON.stringify(value, null, 2)}
; + } + return {String(value)}; +} + +function AttestationTable({ data }: { data: Attestation }) { + return ( +
+ + + {Object.entries(data).map(([key, value]) => ( + + + + + ))} + +
{key} + +
+
+ ); +} + +/** Hash + in-app link without nesting a button inside the `
`. */ +function ModelNameLink({ name }: { name: string }) { + return ( + + + {name} + + + + ); +} + +function PublisherModelTable({ items }: { items: NameListItem[] }) { + if (items.length === 0) { + return

No models indexed for this publisher.

; + } + return ( +
+ + + + + + + + + + + {items.map((item) => ( + + + + + + + ))} + +
ModelLicenseSizeLatest
+ + + + {formatBytes(item.totalSize)}{item.latest.version}
+
+ ); +} + export function PublisherPage() { - return
PublisherPage
; + const { label: rawLabel } = useParams(); + const name = decodePublisherParam(rawLabel); + const client = useClient(); + const { data, error, loading } = useQuery(`publisher:${name ?? ""}`, () => + name === null + ? Promise.resolve({ publisher: null, models: [] as NameListItem[], attestation: null }) + : loadPublisherPage(client, name), + ); + + if (name === null) { + return ; + } + if (loading) return ; + if (error) return ; + if (data === null || data.publisher === null) { + return ; + } + + const { publisher, models, attestation } = data; + const modelLabel = publisher.models === 1 ? "model" : "models"; + + return ( +
+
+

+ +

+

+ {publisher.hf !== null && ( + + {publisher.hf} + HF verified + + )} + + {publisher.models} {modelLabel} + +

+
+ +
+

Attestation

+ {attestation === null ? ( +

+ No registrar attestation. This name is not issued through the enspack.eth registrar + (e.g. mirrors.enspack.eth, or a self-owned .eth). +

+ ) : ( + + )} +
+ +
+

Models

+ +
+
+ ); } diff --git a/apps/site/src/pages/violations.tsx b/apps/site/src/pages/violations.tsx index 7e24643..d6c85c6 100644 --- a/apps/site/src/pages/violations.tsx +++ b/apps/site/src/pages/violations.tsx @@ -1,3 +1,86 @@ +import { Link } from "react-router"; +import { CopyButton } from "../components/copy-button.js"; +import { ExternalLink } from "../components/external-link.js"; +import { Hash } from "../components/hash.js"; +import { EmptyState, ErrorState, Loading } from "../components/states.js"; +import { useClient } from "../lib/client-context.js"; +import { config } from "../lib/config.js"; +import { explorerBlock } from "../lib/format.js"; +import { useQuery } from "../lib/use-query.js"; +import "../styles/pages/violations.css"; + +/** Hash + in-app link without nesting a button inside the `
`. */ +function ViolationNameLink({ name }: { name: string }) { + return ( + + + {name} + + + + ); +} + export function ViolationsPage() { - return
ViolationsPage
; + const client = useClient(); + const { data, error, loading } = useQuery("violations", () => client.listViolations()); + + if (loading) return ; + if (error) return ; + if (data === null || data.items.length === 0) { + return ( + + ); + } + + return ( +
+

Violations

+

+ Version names are immutable by convention: publishers must not change a version's + contenthash after it is first set. The indexer flags any later contenthash change on a + version name as a policy violation (SPEC §6.2). +

+
+ + + + + + + + + + + {data.items.map((v) => ( + + + + + + + ))} + +
NameNodeCIDBlock
+ + + + +
+ + + +
+
+ {v.block} +
+
+
+ ); } diff --git a/apps/site/src/styles/pages/get-started.css b/apps/site/src/styles/pages/get-started.css new file mode 100644 index 0000000..962196a --- /dev/null +++ b/apps/site/src/styles/pages/get-started.css @@ -0,0 +1,83 @@ +.get-started-page h1 { + margin: 0 0 var(--sp-4); + font-size: var(--fs-xl); + font-weight: 600; +} + +.get-started-lead { + margin: 0 0 var(--sp-8); + color: var(--muted); + max-width: 62ch; +} + +.get-started-page section { + margin-bottom: var(--sp-10); +} + +.get-started-page h2 { + margin: 0 0 var(--sp-3); + font-size: var(--fs-lg); + font-weight: 600; +} + +.get-started-page p { + margin: 0 0 var(--sp-3); + max-width: 62ch; +} + +.get-started-page ul { + margin: 0 0 var(--sp-5); + padding-left: 1.25rem; + max-width: 68ch; +} + +.get-started-page li + li { + margin-top: var(--sp-2); +} + +.command-block { + display: flex; + align-items: flex-start; + gap: var(--sp-3); + margin: var(--sp-3) 0 var(--sp-4); + padding: var(--sp-3) var(--sp-4); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.command-block pre { + margin: 0; + flex: 1; + overflow-x: auto; + font-size: var(--fs-sm); + line-height: 1.5; +} + +.command-block code { + font-family: var(--font-mono); +} + +.get-started-cta { + display: inline-block; + margin: var(--sp-2) 0 var(--sp-5); + padding: var(--sp-3) var(--sp-5); + background: var(--accent-soft); + color: var(--accent); + border: 1px solid var(--accent); + border-radius: var(--radius); + font-weight: 600; +} + +.get-started-cta:hover { + text-decoration: none; + background: var(--accent); + color: var(--bg); +} + +.get-started-links { + display: flex; + flex-wrap: wrap; + gap: var(--sp-5); + font-size: var(--fs-sm); +} diff --git a/apps/site/src/styles/pages/home.css b/apps/site/src/styles/pages/home.css new file mode 100644 index 0000000..d532a1e --- /dev/null +++ b/apps/site/src/styles/pages/home.css @@ -0,0 +1,287 @@ +.home { + display: flex; + flex-direction: column; + gap: var(--sp-12); +} + +.home-hero { + display: flex; + flex-direction: column; + gap: var(--sp-4); + max-width: 44rem; +} + +.home-hero-title { + font-size: var(--fs-2xl); + font-weight: 650; + letter-spacing: -0.03em; + line-height: 1.15; + margin: 0; +} + +.home-hero-sub { + margin: 0; + color: var(--muted); + font-size: var(--fs-md); +} + +.command-block { + display: flex; + align-items: center; + gap: var(--sp-3); + flex-wrap: wrap; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-3) var(--sp-4); +} + +.command-block pre { + margin: 0; + flex: 1; + min-width: 0; +} + +.command-block code { + overflow-wrap: anywhere; +} + +.home-stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--sp-4); +} + +.stat { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-5); +} + +.stat-value { + font-family: var(--font-mono); + font-size: var(--fs-xl); + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.stat-label { + color: var(--muted); + font-size: var(--fs-sm); + margin-top: var(--sp-1); +} + +.home-section-title { + font-size: var(--fs-lg); + font-weight: 600; + margin: 0 0 var(--sp-4); +} + +.how-cards { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--sp-4); +} + +.how-card { + display: flex; + flex-direction: column; + gap: var(--sp-3); + min-width: 0; +} + +.how-card-title { + margin: 0; + font-size: var(--fs-sm); + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); +} + +.how-artifact { + display: block; + overflow-wrap: anywhere; + word-break: break-all; + color: var(--text); + background: var(--surface-2); + border-radius: var(--radius); + padding: var(--sp-2) var(--sp-3); +} + +.how-trust { + margin: 0; + color: var(--muted); + font-size: var(--fs-sm); + line-height: 1.45; +} + +.model-list-controls { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: var(--sp-4); + margin-bottom: var(--sp-5); +} + +.model-search { + display: flex; + flex-direction: column; + gap: var(--sp-1); + font-size: var(--fs-sm); + color: var(--muted); + flex: 1 1 16rem; +} + +.model-search input { + font: inherit; + font-family: var(--font-mono); + color: var(--text); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--sp-2) var(--sp-3); +} + +.publisher-chips { + display: flex; + flex-wrap: wrap; + gap: var(--sp-2); + margin: 0; + padding: 0; + border: 0; + min-inline-size: 0; +} + +.publisher-chips legend { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.chip { + display: inline-flex; + align-items: center; + border: 1px solid var(--border); + background: var(--surface); + border-radius: 999px; + padding: var(--sp-1) var(--sp-3); + font-size: var(--fs-xs); + font-family: var(--font-mono); + color: var(--muted); + cursor: pointer; + text-decoration: none; + line-height: 1.5; +} + +a.chip:hover { + text-decoration: none; + color: var(--text); +} + +.chip[aria-pressed="true"] { + color: var(--accent); + background: var(--accent-soft); + border-color: transparent; +} + +.chip-link { + color: var(--text); +} + +.model-table-wrap { + overflow-x: auto; +} + +.model-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.model-table th { + text-align: left; + font-weight: 500; + color: var(--muted); + font-size: var(--fs-xs); + letter-spacing: 0.04em; + text-transform: uppercase; + border-bottom: 1px solid var(--border); + padding: var(--sp-2) var(--sp-3); +} + +.model-table td { + border-bottom: 1px solid var(--border); + padding: var(--sp-4) var(--sp-3); + vertical-align: top; +} + +.model-table td .hash { + display: flex; + margin-top: var(--sp-1); +} + +.model-display { + display: block; + font-weight: 600; + color: var(--text); +} + +.model-display:hover { + color: var(--accent); + text-decoration: none; +} + +.model-table .num { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.load-more { + display: block; + margin: var(--sp-5) auto 0; + border: 1px solid var(--border); + background: var(--surface); + border-radius: var(--radius); + padding: var(--sp-2) var(--sp-5); + cursor: pointer; + color: var(--text); +} + +.load-more:hover:not(:disabled) { + border-color: var(--muted); +} + +.load-more:disabled { + opacity: 0.6; + cursor: default; +} + +@media (max-width: 960px) { + .how-cards { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 640px) { + .home-hero-title { + font-size: var(--fs-xl); + } + + .home-stats, + .how-cards { + grid-template-columns: 1fr; + } +} diff --git a/apps/site/src/styles/pages/model.css b/apps/site/src/styles/pages/model.css new file mode 100644 index 0000000..86ce682 --- /dev/null +++ b/apps/site/src/styles/pages/model.css @@ -0,0 +1,431 @@ +.model-page { + display: flex; + flex-direction: column; + gap: var(--sp-6); +} + +.model-status { + margin: 0; + padding: var(--sp-3) var(--sp-4); + font-size: var(--fs-sm); + color: var(--muted); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.model-header { + display: flex; + flex-direction: column; + gap: var(--sp-3); +} + +.model-title { + margin: 0; + font-size: var(--fs-2xl); + font-weight: 650; + letter-spacing: -0.03em; + line-height: 1.15; +} + +.model-name-hash { + font-size: var(--fs-sm); +} + +.model-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-3); + margin: 0; + font-size: var(--fs-sm); + color: var(--muted); +} + +.model-publisher { + display: inline-flex; + align-items: center; + border: 1px solid var(--border); + background: var(--surface); + border-radius: 999px; + padding: var(--sp-1) var(--sp-3); + font-size: var(--fs-xs); + font-family: var(--font-mono); + color: var(--text); + line-height: 1.5; +} + +.model-publisher:hover { + text-decoration: none; + color: var(--accent); +} + +.model-canonical { + margin: 0; + font-size: var(--fs-sm); + color: var(--muted); +} + +.model-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: var(--sp-8); + align-items: start; +} + +.model-main { + display: flex; + flex-direction: column; + gap: var(--sp-10); + min-width: 0; +} + +@media (max-width: 960px) { + .model-layout { + grid-template-columns: minmax(0, 1fr); + } + + .get-panel { + position: static; + } +} + +.model-page h2 { + margin: 0 0 var(--sp-4); + font-size: var(--fs-lg); + font-weight: 600; +} + +.model-page h3 { + margin: 0 0 var(--sp-3); + font-size: var(--fs-md); + font-weight: 600; +} + +/* Trust chain: 2px rail, accent dots; verify uses --ok. */ +.trust-chain { + list-style: none; + margin: 0; + padding: 0; + position: relative; +} + +.trust-chain::before { + content: ""; + position: absolute; + left: 7px; + top: 6px; + bottom: 6px; + width: 2px; + background: var(--border); +} + +.trust-node { + display: grid; + grid-template-columns: 16px minmax(0, 1fr); + gap: var(--sp-4); + position: relative; + padding-bottom: var(--sp-6); +} + +.trust-node:last-child { + padding-bottom: 0; +} + +.trust-dot { + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--accent); + border: 2px solid var(--bg); + margin-top: 4px; + position: relative; + z-index: 1; +} + +.trust-node-verify .trust-dot { + background: var(--ok); +} + +.trust-body { + min-width: 0; +} + +.trust-caption { + margin: 0 0 var(--sp-1); + font-size: var(--fs-xs); +} + +.trust-names { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--sp-4); +} + +@media (max-width: 640px) { + .trust-names { + grid-template-columns: minmax(0, 1fr); + } +} + +.trust-proves, +.trust-verify { + margin: var(--sp-3) 0 0; + color: var(--muted); + font-size: var(--fs-sm); +} + +.trust-verify { + color: var(--text); +} + +.trust-manifest-kv { + margin: 0; +} + +.trust-json { + margin-top: var(--sp-3); + font-size: var(--fs-sm); +} + +.trust-json summary { + cursor: pointer; + color: var(--accent); +} + +.trust-json pre { + margin: var(--sp-3) 0 0; + padding: var(--sp-3); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow-x: auto; + font-size: var(--fs-xs); + max-height: 24rem; +} + +.trust-bytes { + display: flex; + flex-direction: column; + gap: var(--sp-3); + margin: 0; +} + +.trust-bytes-field { + display: grid; + grid-template-columns: minmax(6rem, 8rem) minmax(0, 1fr); + gap: var(--sp-2) var(--sp-4); + font-size: var(--fs-sm); +} + +.trust-bytes-field dt { + color: var(--muted); + font-weight: 500; +} + +.trust-bytes-field dd { + margin: 0; + min-width: 0; +} + +.trust-link-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--sp-1); +} + +.trust-link-list a, +.trust-link-list code { + overflow-wrap: anywhere; + word-break: break-all; + font-size: var(--fs-xs); +} + +/* Files */ +.files-table-wrap { + max-height: min(70vh, 36rem); + overflow: auto; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); +} + +.files-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.files-table th, +.files-table td { + text-align: left; + padding: var(--sp-2) var(--sp-3); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.files-table thead th { + position: sticky; + top: 0; + z-index: 1; + background: var(--surface-2); + color: var(--muted); + font-weight: 500; + font-size: var(--fs-xs); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.files-table tfoot th, +.files-table tfoot td { + border-bottom: none; + color: var(--muted); + font-weight: 500; +} + +.files-sort { + background: none; + border: 0; + padding: 0; + cursor: pointer; + color: inherit; + font: inherit; + text-transform: inherit; + letter-spacing: inherit; +} + +.files-sort:hover { + color: var(--text); +} + +.files-path { + font-family: var(--font-mono); + font-size: 0.92em; +} + +.files-size { + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.files-sha .hash { + display: flex; +} + +/* Versions */ +.versions-table-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); +} + +.versions-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.versions-table th, +.versions-table td { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.versions-table thead th { + color: var(--muted); + font-weight: 500; + background: var(--surface-2); + font-size: var(--fs-xs); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.versions-table tbody tr:last-child td { + border-bottom: none; +} + +.versions-table td:first-child { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-2); +} + +.versions-name-link { + color: var(--text); +} + +.versions-name-link:hover { + color: var(--accent); + text-decoration: none; +} + +.versions-note { + margin: var(--sp-3) 0 0; + font-size: var(--fs-sm); +} + +/* Get it */ +.get-panel { + position: sticky; + top: var(--sp-4); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--sp-5); +} + +.get-panel h2 { + margin: 0 0 var(--sp-4); +} + +.get-block { + position: relative; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--sp-3); + padding-right: 5.5rem; + margin-bottom: var(--sp-3); +} + +.get-block-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--sp-2); + margin-bottom: var(--sp-2); +} + +.get-block-head .copy-btn { + position: absolute; + top: var(--sp-2); + right: var(--sp-2); +} + +.get-block-label { + font-size: var(--fs-xs); + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.get-block pre { + margin: 0; + font-family: var(--font-mono); + font-size: var(--fs-xs); + overflow-x: auto; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.get-footnote { + margin: var(--sp-2) 0 0; + font-size: var(--fs-xs); +} diff --git a/apps/site/src/styles/pages/publisher.css b/apps/site/src/styles/pages/publisher.css new file mode 100644 index 0000000..f4f11ec --- /dev/null +++ b/apps/site/src/styles/pages/publisher.css @@ -0,0 +1,116 @@ +.publisher-header { + display: flex; + flex-direction: column; + gap: var(--sp-3); + margin-bottom: var(--sp-8); +} + +.publisher-header h1 { + margin: 0; + font-size: var(--fs-xl); + font-weight: 600; + line-height: 1.3; + overflow-wrap: anywhere; +} + +.publisher-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-3); + margin: 0; + color: var(--muted); + font-size: var(--fs-sm); +} + +.publisher-hf { + display: inline-flex; + align-items: center; + gap: var(--sp-2); +} + +.publisher-section { + margin-bottom: var(--sp-8); +} + +.publisher-section h2 { + margin: 0 0 var(--sp-4); + font-size: var(--fs-lg); + font-weight: 600; +} + +.publisher-attestation-note { + margin: 0; + color: var(--muted); + max-width: 62ch; +} + +.publisher-table-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); +} + +.publisher-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.publisher-table th, +.publisher-table td { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.publisher-table thead th { + color: var(--muted); + font-weight: 500; + background: var(--surface-2); +} + +.publisher-table tbody tr:last-child th, +.publisher-table tbody tr:last-child td { + border-bottom: none; +} + +.publisher-table th[scope="row"] { + font-family: var(--font-mono); + font-size: var(--fs-xs); + font-weight: 500; + color: var(--muted); + white-space: nowrap; + width: 12rem; +} + +.attestation-list { + display: flex; + flex-direction: column; + gap: var(--sp-2); +} + +.attestation-pre { + margin: 0; + padding: var(--sp-3); + background: var(--surface-2); + border-radius: var(--radius); + overflow-x: auto; + font-size: var(--fs-xs); +} + +.publisher-name-link { + color: var(--text); +} + +.publisher-name-link:hover { + color: var(--accent); + text-decoration: none; +} + +.publisher-empty-models { + margin: 0; + color: var(--muted); +} diff --git a/apps/site/src/styles/pages/violations.css b/apps/site/src/styles/pages/violations.css new file mode 100644 index 0000000..f3a616d --- /dev/null +++ b/apps/site/src/styles/pages/violations.css @@ -0,0 +1,62 @@ +.violations-page h1 { + margin: 0 0 var(--sp-4); + font-size: var(--fs-xl); + font-weight: 600; +} + +.violations-intro { + margin: 0 0 var(--sp-6); + color: var(--muted); + max-width: 68ch; +} + +.violations-table-wrap { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); +} + +.violations-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} + +.violations-table th, +.violations-table td { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.violations-table thead th { + color: var(--muted); + font-weight: 500; + background: var(--surface-2); +} + +.violations-table tbody tr:last-child td { + border-bottom: none; +} + +.violations-cids { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-2); +} + +.violations-arrow { + color: var(--muted); +} + +.violations-name-link { + color: var(--text); +} + +.violations-name-link:hover { + color: var(--accent); + text-decoration: none; +} diff --git a/apps/site/test/get-started.test.tsx b/apps/site/test/get-started.test.tsx new file mode 100644 index 0000000..1d465b5 --- /dev/null +++ b/apps/site/test/get-started.test.tsx @@ -0,0 +1,17 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { config } from "../src/lib/config.js"; +import { GetStartedPage } from "../src/pages/get-started.js"; +import { renderAt } from "./helpers.js"; + +describe("GetStartedPage", () => { + it("renders the registrar CTA and the enspack get command", () => { + renderAt("/get-started", ); + + const cta = screen.getByRole("link", { name: /Claim \.enspack\.eth/ }); + expect(cta.getAttribute("href")?.startsWith(config.registrarUrl)).toBe(true); + expect( + screen.getByText("enspack get qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"), + ).toBeInTheDocument(); + }); +}); diff --git a/apps/site/test/home.test.tsx b/apps/site/test/home.test.tsx new file mode 100644 index 0000000..89c8e12 --- /dev/null +++ b/apps/site/test/home.test.tsx @@ -0,0 +1,74 @@ +import { cleanup, fireEvent, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { FixtureIndexClient, type IndexClient } from "../src/lib/client.js"; +import { HomePage } from "../src/pages/home.js"; +import { renderAt } from "./helpers.js"; + +afterEach(() => { + cleanup(); +}); + +const QWEN = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; +const LLAMA = "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth"; +const JEFF = "tiny-random.jeff.enspack.eth"; + +function rejectingClient(): IndexClient { + const boom = () => Promise.reject(new Error("indexer down")); + return { + listNames: boom, + getName: boom, + listPublishers: boom, + listViolations: boom, + getAttestation: boom, + }; +} + +describe("HomePage", () => { + it("renders three models from fixtures", async () => { + const { findByText, getByText } = renderAt("/", ); + expect(await findByText(QWEN)).toBeInTheDocument(); + expect(getByText(LLAMA)).toBeInTheDocument(); + expect(getByText(JEFF)).toBeInTheDocument(); + }); + + it("narrows the list to one model when searching Qwen", async () => { + const { findByText, getByLabelText, queryByText } = renderAt("/", ); + await findByText(QWEN); + fireEvent.change(getByLabelText("Search"), { target: { value: "Qwen" } }); + await waitFor(() => { + expect(queryByText(LLAMA)).not.toBeInTheDocument(); + expect(queryByText(JEFF)).not.toBeInTheDocument(); + }); + expect(queryByText(QWEN)).toBeInTheDocument(); + }); + + it("filters by publisher chip", async () => { + const { findByRole, getByText, queryByText } = renderAt("/", ); + const chip = await findByRole("button", { name: "mirrors.enspack.eth" }); + fireEvent.click(chip); + await waitFor(() => { + expect(queryByText(JEFF)).not.toBeInTheDocument(); + }); + expect(getByText(QWEN)).toBeInTheDocument(); + expect(getByText(LLAMA)).toBeInTheDocument(); + }); + + it("renders the empty state when the index has no names", async () => { + const client = new FixtureIndexClient({ + names: { items: [], nextCursor: null }, + details: {}, + publishers: { items: [] }, + violations: { items: [] }, + attestations: {}, + }); + const { findByText } = renderAt("/", , { client }); + expect(await findByText("No names indexed yet on sepolia.")).toBeInTheDocument(); + expect(await findByText(/enspack publish --from-hf/)).toBeInTheDocument(); + }); + + it("renders the error state when the client rejects", async () => { + const { findByRole } = renderAt("/", , { client: rejectingClient() }); + const alert = await findByRole("alert"); + expect(alert).toHaveTextContent("Could not load models"); + }); +}); diff --git a/apps/site/test/model.test.tsx b/apps/site/test/model.test.tsx new file mode 100644 index 0000000..d4e51f6 --- /dev/null +++ b/apps/site/test/model.test.tsx @@ -0,0 +1,131 @@ +import { cleanup, fireEvent, screen, within } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; +import { ensAppUrl } from "../src/lib/format.js"; +import { ModelPage } from "../src/pages/model.js"; +import { renderAt } from "./helpers.js"; + +afterEach(() => { + cleanup(); +}); + +const QWEN = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; +const QWEN_VERSION = "v1-0-0.qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; +const JEFF = "tiny-random.jeff.enspack.eth"; +const JEFF_OLD = "v0-1-0.tiny-random.jeff.enspack.eth"; + +const QWEN_PATHS = [ + ".gitattributes", + "LICENSE", + "README.md", + "config.json", + "generation_config.json", + "merges.txt", + "model-00001-of-00004.safetensors", + "model-00002-of-00004.safetensors", + "model-00003-of-00004.safetensors", + "model-00004-of-00004.safetensors", + "model.safetensors.index.json", + "tokenizer.json", + "tokenizer_config.json", + "vocab.json", +] as const; + +const QWEN_SHA = "11ad7efa24975ee4b0c3c3a38ed18737f0658a5f75a0a96787b576a78a023361"; +const QWEN_INFOHASH = "136cb94838da83a906263ec23d03dce95d736c64"; +const QWEN_MAGNET = + "magnet:?xt=urn:btih:136cb94838da83a906263ec23d03dce95d736c64&dn=Qwen2.5-7B-Instruct"; +const QWEN_WEBSEEDS = [ + "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/resolve/a09a35458c702b33eeacc393d103063234e8bc28/", + "https://huggingbay.xyz/api/downloads/hf-model-qwen-qwen2-5-7b-instruct/", +] as const; + +function renderName(name: string) { + return renderAt(`/name/${encodeURIComponent(name)}`, , { + routePattern: "/name/:name", + }); +} + +function filePaths(): string[] { + const table = screen.getByRole("table", { name: "Files" }); + const tbody = table.querySelector("tbody"); + if (tbody === null) throw new Error("expected files tbody"); + return [...tbody.querySelectorAll("tr")].map((row) => { + const path = within(row).getAllByRole("cell")[0]?.textContent?.trim() ?? ""; + return path; + }); +} + +describe("ModelPage", () => { + it("renders the Qwen trust chain, files, and lock entry", async () => { + renderName(QWEN); + + expect(await screen.findByRole("heading", { name: "Qwen2.5-7B-Instruct" })).toBeInTheDocument(); + expect( + screen.getByText( + "Indexed from sepolia. Verification shown here is the indexer's; the CLI re-verifies every file locally.", + ), + ).toBeInTheDocument(); + + for (const path of QWEN_PATHS) { + expect(screen.getByText(path)).toBeInTheDocument(); + } + expect(QWEN_PATHS).toHaveLength(14); + expect(screen.getByText(QWEN_SHA)).toBeInTheDocument(); + expect(screen.getByText(QWEN_INFOHASH)).toBeInTheDocument(); + + const magnet = document.querySelector(`a[href="${QWEN_MAGNET}"]`); + expect(magnet).not.toBeNull(); + + for (const url of QWEN_WEBSEEDS) { + expect(screen.getByRole("link", { name: url })).toHaveAttribute("href", url); + } + + const ensHref = ensAppUrl("sepolia", QWEN_VERSION); + const ensLink = document.querySelector(`a[href="${ensHref}"]`); + expect(ensLink).not.toBeNull(); + + expect(screen.getByText(/"lockfileVersion": 1/)).toBeInTheDocument(); + }); + + it("marks Jeff's latest version and links the older version name", async () => { + renderName(JEFF); + expect(await screen.findByRole("heading", { name: "tiny-random" })).toBeInTheDocument(); + + const versions = screen.getByRole("table", { name: "Versions" }); + const latestRow = versions.querySelector("tr[data-latest='true']"); + expect(latestRow).toBeInstanceOf(HTMLTableRowElement); + if (!(latestRow instanceof HTMLTableRowElement)) throw new Error("expected latest row"); + expect(within(latestRow).getByText("latest")).toBeInTheDocument(); + expect(within(latestRow).getByText("0.2.0")).toBeInTheDocument(); + + expect(screen.getByRole("link", { name: JEFF_OLD })).toHaveAttribute( + "href", + `/name/${encodeURIComponent(JEFF_OLD)}`, + ); + }); + + it("resolves a version-name URL to the model", async () => { + renderName(JEFF_OLD); + expect(await screen.findByRole("heading", { name: "tiny-random" })).toBeInTheDocument(); + expect(screen.getAllByText(JEFF).length).toBeGreaterThan(0); + }); + + it("shows the unknown-name state", async () => { + renderName("nope.eth"); + expect(await screen.findByText("Unknown name")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Home" })).toHaveAttribute("href", "/"); + }); + + it("reorders file rows when sorting by size", async () => { + renderName(QWEN); + await screen.findByRole("heading", { name: "Qwen2.5-7B-Instruct" }); + + const before = filePaths(); + expect(before[0]).toBe(".gitattributes"); + + fireEvent.click(screen.getByRole("button", { name: "size" })); + const after = filePaths(); + expect(after).not.toEqual(before); + expect(after[0]).toBe("generation_config.json"); + }); +}); diff --git a/apps/site/test/publisher.test.tsx b/apps/site/test/publisher.test.tsx new file mode 100644 index 0000000..67e5bd9 --- /dev/null +++ b/apps/site/test/publisher.test.tsx @@ -0,0 +1,43 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { PublisherPage } from "../src/pages/publisher.js"; +import { renderAt } from "./helpers.js"; + +const route = { routePattern: "/publisher/:label" }; + +describe("PublisherPage", () => { + it("shows the no-attestation sentence and two models for mirrors", async () => { + renderAt("/publisher/mirrors.enspack.eth", , route); + + expect( + await screen.findByText(/No registrar attestation\. This name is not issued through/), + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth" }), + ).toHaveAttribute("href", "/name/qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"); + expect( + screen.getByRole("link", { + name: "meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth", + }), + ).toHaveAttribute("href", "/name/meta-llama--llama-3-2-1b-instruct.mirrors.enspack.eth"); + }); + + it("shows the HF namespace, verified badge, and attestation table for jeff", async () => { + renderAt("/publisher/jeff.enspack.eth", , route); + + const hf = await screen.findByRole("link", { name: "jefflau" }); + expect(hf).toHaveAttribute("href", "https://huggingface.co/jefflau"); + expect(screen.getByText("HF verified")).toBeInTheDocument(); + expect(screen.getByText("jefflau/enspack-verify")).toBeInTheDocument(); + + const txLinks = screen + .getAllByRole("link") + .filter((el) => el.getAttribute("href")?.includes("etherscan.io/tx/")); + expect(txLinks.length).toBeGreaterThan(0); + }); + + it("shows the unknown-publisher empty state", async () => { + renderAt("/publisher/nope.eth", , route); + expect(await screen.findByText("Unknown publisher")).toBeInTheDocument(); + }); +}); diff --git a/apps/site/test/violations.test.tsx b/apps/site/test/violations.test.tsx new file mode 100644 index 0000000..91e43f3 --- /dev/null +++ b/apps/site/test/violations.test.tsx @@ -0,0 +1,35 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import type { ViolationsPage } from "../src/lib/api.js"; +import { FixtureIndexClient } from "../src/lib/client.js"; +import { ViolationsPage as ViolationsPageView } from "../src/pages/violations.js"; +import violationsOne from "./fixtures/violations-one.json"; +import { demoFixtures, renderAt } from "./helpers.js"; + +describe("ViolationsPage", () => { + it("shows the empty-state sentence when there are no violations", async () => { + renderAt("/violations", ); + expect( + await screen.findByText( + "No violations. No version name has changed its contenthash after it was first set.", + ), + ).toBeInTheDocument(); + }); + + it("renders a violation row with a name link and a sepolia block explorer link", async () => { + const client = new FixtureIndexClient({ + ...demoFixtures, + violations: violationsOne as ViolationsPage, + }); + renderAt("/violations", , { client }); + + const nameLink = await screen.findByRole("link", { + name: "v0-1-0.tiny-random.jeff.enspack.eth", + }); + expect(nameLink).toHaveAttribute("href", "/name/v0-1-0.tiny-random.jeff.enspack.eth"); + expect(screen.getByRole("link", { name: "9123456" })).toHaveAttribute( + "href", + "https://sepolia.etherscan.io/block/9123456", + ); + }); +}); From 564d5ce6d46a8eb7f409cc2317e181fe4b4afe0c Mon Sep 17 00:00:00 2001 From: Jeff Lau Date: Mon, 14 Sep 2026 16:47:34 +0000 Subject: [PATCH 5/9] feat(site): add useDocumentTitle hook Co-authored-by: Jeff Lau --- apps/site/src/lib/use-document-title.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/site/src/lib/use-document-title.ts diff --git a/apps/site/src/lib/use-document-title.ts b/apps/site/src/lib/use-document-title.ts new file mode 100644 index 0000000..587f649 --- /dev/null +++ b/apps/site/src/lib/use-document-title.ts @@ -0,0 +1,15 @@ +import { useEffect } from "react"; + +const SUFFIX = "enspack"; + +/** Sets `document.title` for the current page; pass `null` while data is still loading. */ +export function useDocumentTitle(title: string | null): void { + useEffect(() => { + if (title === null) return; + const previous = document.title; + document.title = title === "" ? SUFFIX : `${title} · ${SUFFIX}`; + return () => { + document.title = previous; + }; + }, [title]); +} From 899b7d1eb8e21e04b6d84724c7b6ed92f83594ca Mon Sep 17 00:00:00 2001 From: Jeff Lau Date: Mon, 14 Sep 2026 16:47:34 +0000 Subject: [PATCH 6/9] chore: regenerate pnpm-lock after rebase onto master (infra workspace) Co-authored-by: Jeff Lau --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad8b448..2415964 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,7 +111,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.7(@types/node@22.20.2)(yaml@2.9.1) + version: 3.2.7(@types/node@22.20.2)(jsdom@26.1.0)(yaml@2.9.1) yaml: specifier: ^2.7.0 version: 2.9.1 From aa665afd6e2aab7cf95fab2d8b3e459587dbfe10 Mon Sep 17 00:00:00 2001 From: Jeff Lau Date: Mon, 14 Sep 2026 16:47:34 +0000 Subject: [PATCH 7/9] fix(site): responsive and consistency polish from visual QA Files table stacks under 720px, trust chain and attestation values wrap, lockfile entry collapses by default, hero and stats stay on the unfiltered catalog, shared code-block chrome, primary CTA, per-route document titles. Co-authored-by: Jeff Lau --- apps/site/src/components/command-block.tsx | 4 +- apps/site/src/components/files-table.tsx | 10 +- apps/site/src/components/get-panel.tsx | 5 +- apps/site/src/components/hero.tsx | 9 +- apps/site/src/components/model-list.tsx | 33 ++-- apps/site/src/components/stats.tsx | 2 +- apps/site/src/pages/get-started.tsx | 66 +++++--- apps/site/src/pages/home.tsx | 13 +- apps/site/src/pages/model.tsx | 3 + apps/site/src/pages/not-found.tsx | 2 + apps/site/src/pages/publisher.tsx | 12 +- apps/site/src/pages/violations.tsx | 9 +- apps/site/src/styles/global.css | 44 ++++- apps/site/src/styles/pages/get-started.css | 40 +---- apps/site/src/styles/pages/home.css | 67 +++++--- apps/site/src/styles/pages/model.css | 179 ++++++++++++++++++++- apps/site/src/styles/pages/publisher.css | 33 +++- apps/site/test/get-started.test.tsx | 8 +- apps/site/test/home.test.tsx | 31 +++- apps/site/test/model.test.tsx | 8 +- apps/site/test/publisher.test.tsx | 14 +- apps/site/test/violations.test.tsx | 10 +- 22 files changed, 465 insertions(+), 137 deletions(-) diff --git a/apps/site/src/components/command-block.tsx b/apps/site/src/components/command-block.tsx index 4cc905f..f6a2e3d 100644 --- a/apps/site/src/components/command-block.tsx +++ b/apps/site/src/components/command-block.tsx @@ -1,9 +1,9 @@ import { CopyButton } from "./copy-button.js"; -/** Copyable `
` for CLI snippets. Styled by the importing page (get-started.css). */
+/** Copyable CLI snippet; chrome comes from global `.cmd`. */
 export function CommandBlock({ command }: { command: string }) {
   return (
-    
+
         {command}
       
diff --git a/apps/site/src/components/files-table.tsx b/apps/site/src/components/files-table.tsx index 60c6f2f..28e61d4 100644 --- a/apps/site/src/components/files-table.tsx +++ b/apps/site/src/components/files-table.tsx @@ -70,14 +70,16 @@ export function FilesTable({ {rows.map((file) => ( - + {file.path} - + - {formatBytes(file.size)} - + + {formatBytes(file.size)} + + diff --git a/apps/site/src/components/get-panel.tsx b/apps/site/src/components/get-panel.tsx index 118c4a6..2ad4d76 100644 --- a/apps/site/src/components/get-panel.tsx +++ b/apps/site/src/components/get-panel.tsx @@ -56,7 +56,10 @@ export function GetPanel({ manifest, cid }: { manifest: Manifest; cid: string })

Get it

- +
+ enspack.lock entry + +

Requires aria2c. Installs into $HF_HOME/hub.

diff --git a/apps/site/src/components/hero.tsx b/apps/site/src/components/hero.tsx index 861eb1f..eb512bb 100644 --- a/apps/site/src/components/hero.tsx +++ b/apps/site/src/components/hero.tsx @@ -1,4 +1,4 @@ -import { CopyButton } from "./copy-button.js"; +import { CommandBlock } from "./command-block.js"; const QWEN_EXAMPLE = "qwen--qwen2-5-7b-instruct.mirrors.enspack.eth"; @@ -12,12 +12,7 @@ export function Hero({ firstModel }: { firstModel?: string }) { An ENS name points at a content-addressed manifest; every file is SHA-256 checked; bytes come from BitTorrent + HF webseeds, never trusted.

-
-
-          {command}
-        
- -
+ ); } diff --git a/apps/site/src/components/model-list.tsx b/apps/site/src/components/model-list.tsx index 3c852c3..5c3e385 100644 --- a/apps/site/src/components/model-list.tsx +++ b/apps/site/src/components/model-list.tsx @@ -45,16 +45,29 @@ export function ModelList({ Models
- +
+ +
+ onQChange(e.target.value)} + placeholder="Name, org, or upstream" + autoComplete="off" + /> + {qInput !== "" && ( + + )} +
+
Filter by publisher