diff --git a/.gitignore b/.gitignore index af192a4..e449e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ dist-ssr *.sw? .vercel .playwright-cli + +# Impeccable design-hook session cache (machine-local) +.impeccable/hook.cache.json diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100644 index 0000000..4fc6250 --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1,14 @@ +{ + "detector": { + "ignoreRules": [], + "ignoreFiles": [], + "ignoreValues": [ + { + "rule": "overused-font", + "value": "geist mono", + "createdAt": "2026-08-17T13:42:35.010Z", + "reason": "user confirmed: owner requires the explorer to match gitlawb.com, whose live site computes font-family 'Geist Mono' on body and h1 (extracted 2026-08-17)" + } + ] + } +} diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..f5b28f8 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,81 @@ +# Product + + + +## Platform + +web + +## Users + +**Primary: developers evaluating the gitlawb network.** Someone has heard about gitlawb — from a shared repo link, the docs, a mention of "signed git pushes without a blockchain" — and arrives wanting to judge whether it is real and worth their time. They are skeptical by default. They are looking for proof of life: real repositories with real names, pushes that happened minutes ago, signatures they could verify themselves. The job is *forming a judgment*, not completing a task. Credibility and comprehension at first contact outrank throughput. + +Secondary audiences exist and are served, but do not steer design decisions: + +- **AI agents and CLI clients**, which never execute JavaScript and receive raw markdown instead of the SPA shell (see Capabilities). They are a real and deliberate audience, but they consume a text representation, not the interface. +- **Node operators** and **agent developers** who use the peers, network, events, and tasks routes for monitoring and traceability. Their needs are legitimate but secondary; when a layout must choose, it chooses the evaluator. + +## Product Purpose + +A live, read-only web explorer for a single gitlawb node. It renders the node's REST API — repositories, code, commits, pulls, issues, push events, signed certificates, registered agents, peers, and delegated tasks — as a browsable surface. + +Its real function is evidentiary. Anyone can claim to run a decentralized git network; the explorer is the artifact that demonstrates one is actually running, populated, and signing. Success is a skeptical developer leaving convinced the network is real and understanding what makes it different. + +## Positioning + +gitlawb's mechanism is verifiable provenance without accounts or a blockchain: identity is an Ed25519 DID keypair generated on the user's machine, every push is signed under RFC 9421 HTTP signatures, and every push produces a ref-update certificate anyone can verify independently. Repositories are cloned over a `gitlawb://` scheme via the `git-remote-gitlawb` helper. Public nodes gate registration behind iCaptcha, a proof-of-intelligence challenge, rather than proof-of-work or human-only CAPTCHAs — agents are first-class participants, not adversaries to be filtered out. + +A conventional forge cannot truthfully copy this: its authority comes from an account system and a server that vouches for history. gitlawb's comes from signatures anyone can check without trusting the node that served them. + +The explorer's job is to make that mechanism *visible* rather than merely stated. Certificates, pushers, DIDs, and cert IDs are not incidental metadata on this surface — they are the evidence. + +## Operating Context + +- The explorer is a pure client of one node's REST API (`node.gitlawb.com`). It holds no database and no server state of its own. +- Nodes serve no CORS headers, so every request goes through a same-origin proxy: Vite's dev proxy locally, Vercel rewrites in production. Peer nodes are reached through per-node path prefixes. This list is duplicated in three files and must be kept in sync. +- Data arrives from a live public network. Repository names, descriptions, and owner DIDs are attacker-influenceable strings and must be treated as untrusted in both rendering and routing. +- Much of the live corpus is machine-generated: at time of writing, roughly 5,900 repositories, many named `pixel-655812`, `astro-505073`, described as "automated repo" or "mirrored from peer", all on branch `main`, all public. The interface's real content is repetitive by nature. Designs validated against curated sample data will not survive contact with it. +- Users pair the explorer with the `gl` CLI and plain `git`; the site's docs and `skill.md` are the installation path. +- Deployed on Vercel. Repository and docs routes are intercepted by serverless functions that inject per-page social metadata into the built SPA shell and generate per-repository OG cards. + +## Capabilities and Constraints + +**Current functionality.** Ten routes: overview, repositories index, repository detail, network, peers, events, tasks, task detail, agents, docs. Repository detail carries file tree, code viewer with syntax highlighting and line-range permalinks, README rendering, commits, pulls, issues, push events, and certificates. Keyboard-first affordances: command palette, fuzzy file finder, shortcuts cheatsheet. + +**Read-only today; write actions are planned.** Star, watch, open an issue, and comment on a pull request are expected to land later. Layouts should reserve credible homes for those affordances now rather than treat every row and header as terminal. No auth or account system exists yet, and the design must not imply one before it does. + +**Technical constraints that bind design:** + +- Repository listing is server-paginated via the `X-Total-Count` header; the node clamps `limit` to 200. Ref-update event limit is likewise clamped to 200. +- Full-index search and sort require node support for `q=`/`sort=` and are opt-in behind `VITE_SERVER_SEARCH`. When off, search and sort apply only to the loaded page, and the interface must say so rather than implying global results. +- Text clients that do not advertise `text/html` receive raw markdown instead of the SPA shell; social and search crawlers are recognized by user agent and receive HTML. Any change to page structure must preserve both representations. +- Agents are unpaginated upstream: fetched once per session and paged client-side. +- Blob classification relies on file extension plus NUL-byte sniffing, because the node guesses content type from extension. SVG is deliberately excluded from inline image rendering. +- Sub-fetches degrade independently: a failed corner of a page renders as unavailable rather than failing the whole page. Empty, partial, and unreachable states are ordinary conditions here, not edge cases. + +**Terminology (product vocabulary, not decoration):** DID, cert / ref-update certificate, push, gossip, peer, node, agent, trust tier (newcomer / contributor / trusted / maintainer), task, delegator, capability, UCAN, iCaptcha. + +## Brand Commitments + +- **Name:** gitlawb. The site is "gitlawb node explorer" at `explorer.gitlawb.com`. +- **Mark:** the official gitlawb glyph, already extracted from the platform brand asset and shipped at [Logo.tsx](src/components/ui/Logo.tsx) and [favicon.svg](public/favicon.svg). Binding — carry it forward. +- **Visual register: modern product showcase.** The explorer is a shop window for the network as much as a tool — it has to look impressive to a developer meeting gitlawb for the first time. Motion, depth, and visual ambition are wanted, delivered through the Magic UI and Aceternity component libraries the user chose. Data density and readability still bind on list-heavy surfaces (repos index, repo detail), but the overview is allowed to sell. +- **Superseded (do not revive):** + - *"The Register"* — light safety-paper ground, engraved guilloche from hashes, wax-seal marks, Bodoni display. Rejected as unprofessional for this audience. + - *Plain forge convention* — a straight GitHub/Sourcegraph rendition. Correct but judged too plain: "we need to make modern which need to showcase". The conventional structure survives on dense list surfaces; the flat, unaccented treatment does not. +- **Libraries are load-bearing.** Aceternity, Magic UI, and GSAP were chosen by the user and are to be used as real components, not reimplemented by hand. + +## Evidence on Hand + +- A live, populated node at `node.gitlawb.com` — real repositories, real signed pushes, real agents and certificates. This is the product's strongest asset and the interface's actual content. +- Written documentation in `public/docs/` (quickstart, agents, protocol, node), plus `llms.txt` and a packaged agent skill at `public/skill.md`. Factual and specific; usable as copy source. +- Screenshots of the v0.3.8 interface in `reference/`, predating the current code. +- **Absent — must not be fabricated:** testimonials, named users or customers, adoption or traffic numbers, benchmarks, funding, pricing, uptime or SLA claims, security audits, and any endorsement. The network is described in its own docs as alpha. No claim of scale or production-readiness beyond what the live API returns is available. + +## Product Principles + +1. **Evidence over assertion.** The interface's persuasive power comes from showing live, checkable facts — a cert ID, a pusher DID, a push two minutes old — not from adjectives about decentralization. Where a claim and a datum compete for space, the datum wins. +2. **Design for the corpus that exists.** Repetitive machine-generated repositories are the typical case, not the degenerate one. Constant-valued fields carry no information and must not consume prominence. +3. **The skeptic's questions come first.** Is this real, is it live, who pushed this, can I verify it, how do I get it locally. A layout that answers those before it decorates is correct. +4. **Two representations, one truth.** Browsers get the interface; agents and CLIs get markdown of the same facts. Neither is a degraded version of the other. +5. **Degradation is a designed state.** Unreachable nodes, partial loads, and empty results are routine on a live P2P network. They deserve real design, not a spinner and an apology. diff --git a/api/_tests/docs-page.test.ts b/api/_tests/docs-page.test.ts index 2daf7ea..5c06acf 100644 --- a/api/_tests/docs-page.test.ts +++ b/api/_tests/docs-page.test.ts @@ -24,24 +24,40 @@ beforeEach(() => { vi.stubEnv('PUBLIC_SITE_URL', ''); }); -/** Serves index.html and public/docs/*.md the way the deployment would. */ -function stubStaticFetch(): ReturnType { - const fetchMock = vi.fn().mockImplementation((input: URL | RequestInfo) => { +/** + * Serves index.html and public/docs/*.md the way the deployment does. + * + * The important detail is that **nothing 404s**. vercel.json rewrites + * `/docs/:slug` into this function — and `:slug` matches a segment containing + * a dot — while `/(.*)` catches everything else and serves index.html. A + * missing `public/docs/.md` therefore does not produce a 404: it is + * rewritten back into the handler. An earlier version of this stub returned + * 404 for absent files, which is why a real bug went unnoticed — the handler + * served the hub index as though it were the requested document. + */ +function stubStaticFetch(present: readonly string[] = ['agents']): ReturnType { + const fetchMock = vi.fn().mockImplementation(async (input: URL | RequestInfo, init?: RequestInit) => { const url = String(input instanceof Request ? input.url : input); - if (url.endsWith('/docs/agents.md')) { - return Promise.resolve( - new Response(AGENTS_MARKDOWN, { + const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); + + const docMatch = url.match(/\/docs\/([^/]+)\.md$/); + if (docMatch) { + if (present.includes(docMatch[1])) { + return new Response(AGENTS_MARKDOWN, { status: 200, headers: { 'Content-Type': 'text/markdown' }, - }), + }); + } + // Filesystem miss → vercel.json rewrites `/docs/.md` back into + // this same function with slug=".md". + const rewritten = new Request( + `https://preview.example/api/docs-page?${new URLSearchParams({ slug: `${docMatch[1]}.md` })}`, + { headers }, ); + return GET(rewritten); } - if (/\/docs\/[^/]+\.md$/.test(url)) { - return Promise.resolve(new Response('not found', { status: 404 })); - } - return Promise.resolve( - new Response(INDEX_HTML, { status: 200, headers: { 'Content-Type': 'text/html' } }), - ); + + return new Response(INDEX_HTML, { status: 200, headers: { 'Content-Type': 'text/html' } }); }); vi.stubGlobal('fetch', fetchMock); return fetchMock; @@ -194,4 +210,68 @@ describe('docs page markdown negotiation', () => { expect(response.headers.get('content-type')).toBe('text/markdown; charset=utf-8'); expect(body).toContain('https://preview.example/docs/agents.md'); }); + + it('never serves the hub index in place of a listed document', async () => { + // The file for "protocol" is absent, so the subrequest is rewritten back + // into this handler. Before the fix that answered 200 with the hub index, + // which was then served as the protocol document — wrong content, correct + // content-type, cached for an hour. + stubStaticFetch(['agents']); + + const response = await GET(docsRequest('protocol', { accept: '*/*' })); + const body = await response.text(); + + expect(body).not.toContain('# gitlawb docs'); + expect(body).not.toContain('All guides are served as raw markdown'); + expect(response.status).toBe(404); + }); + + it('does not cache the response for an absent document', async () => { + stubStaticFetch(['agents']); + + const response = await GET(docsRequest('node', { accept: '*/*' })); + + expect(response.status).toBe(404); + expect(response.headers.get('cache-control')).toBe('private, no-store, max-age=0'); + }); + + it('still serves a document whose file is present', async () => { + stubStaticFetch(['agents']); + + const response = await GET(docsRequest('agents', { accept: '*/*' })); + const body = await response.text(); + + expect(response.status).toBe(200); + expect(body).toBe(AGENTS_MARKDOWN); + }); + + it('rejects the internal markdown subrequest rather than answering it', async () => { + // Reaching the handler with this header means the static file was missing, + // because a file that exists is served before rewrites are consulted. + stubStaticFetch(); + + const response = await GET( + docsRequest('agents.md', { accept: '*/*', 'x-gitlawb-doc-markdown': '1' }), + ); + + expect(response.status).toBe(404); + expect(await response.text()).not.toContain('# gitlawb docs'); + }); + + it('rejects an HTML shell served in place of a document', async () => { + // The other rewrite outcome: the `/(.*)` catch-all serves index.html, so + // the body is the SPA shell rather than markdown. + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue( + new Response(INDEX_HTML, { status: 200, headers: { 'Content-Type': 'text/html' } }), + ), + ); + + const response = await GET(docsRequest('agents', { accept: '*/*' })); + const body = await response.text(); + + expect(response.status).toBe(404); + expect(body).not.toContain('
'); + }); }); diff --git a/api/docs-page.ts b/api/docs-page.ts index 2a0dd95..df68d76 100644 --- a/api/docs-page.ts +++ b/api/docs-page.ts @@ -157,6 +157,16 @@ function shellUnavailable(): Response { }); } +/** + * A line unique to the generated hub index. + * + * `fetchDocMarkdown` checks for it: when a document's static file is absent the + * subrequest can be rewritten back into this function and answered with the hub + * index at status 200, which would then be served as though it were the + * requested document. + */ +const HUB_MARKER = 'All guides are served as raw markdown at stable URLs'; + /** Markdown index served to text clients at the bare /docs route. */ function hubMarkdown(origin: string): string { const lines = [ @@ -164,7 +174,7 @@ function hubMarkdown(origin: string): string { '', '> gitlawb is a decentralized git network where AI agents and humans collaborate as equals. Identity is an Ed25519 DID keypair; every push is signed and produces a verifiable ref-update certificate.', '', - 'All guides are served as raw markdown at stable URLs — fetch these directly:', + `${HUB_MARKER} — fetch these directly:`, '', ]; for (const [slug, meta] of Object.entries(DOCS)) { @@ -185,7 +195,40 @@ function hubMarkdown(origin: string): string { const MARKDOWN_FETCH_TIMEOUT_MS = 3_500; -async function fetchDocMarkdown(request: Request, slug: string): Promise { +/** + * Marks the subrequest this function makes for a document's markdown. + * + * vercel.json has no path that 404s: `/docs/:slug` rewrites here and `:slug` + * matches a segment containing a dot, and anything else falls to the `/(.*)` + * catch-all serving index.html. So when `/docs/.md` misses the + * filesystem, the subrequest below does not fail — it is rewritten back into + * this function, which answers 200 with the hub index (the subrequest's Accept + * is not html), and that would then be served to the client as the requested + * document, with `Content-Type: text/markdown` and an hour of CDN caching. + * + * A request carrying this header therefore means exactly one thing: the static + * file was not there, because a file that exists is served before rewrites are + * consulted. Answering 404 breaks the loop at the first hop. + */ +const INTERNAL_MARKDOWN_HEADER = 'x-gitlawb-doc-markdown'; + +type MarkdownResult = + | { ok: true; markdown: string } + | { ok: false; reason: 'missing' | 'unavailable' }; + +/** + * Whether a fetched body is a document rather than something a rewrite + * substituted. `fetchIndexHtml` guards its own fetch the same way, by testing + * the body instead of trusting the status. + */ +function isDocumentBody(text: string): boolean { + const head = text.slice(0, 500).trimStart(); + if (/^<(!doctype|html\b|\?xml)/i.test(head)) return false; // the SPA shell + if (text.includes(HUB_MARKER)) return false; // this function's own hub index + return true; +} + +async function fetchDocMarkdown(request: Request, slug: string): Promise { const controller = new AbortController(); const timeout = setTimeout( () => controller.abort(new Error('Markdown request timed out')), @@ -195,14 +238,19 @@ async function fetchDocMarkdown(request: Request, slug: string): Promise { const slug = docSlug(request); const origin = siteOrigin(request); + // The markdown subrequest was rewritten back into this function, which means + // the static file it asked for does not exist. Answer 404 rather than the hub + // index, which the caller would otherwise serve as the document itself. + if (request.headers.get(INTERNAL_MARKDOWN_HEADER) !== null) { + return new Response('Not found\n', { + status: 404, + headers: { + 'Cache-Control': NO_CACHE, + 'Content-Type': 'text/plain; charset=utf-8', + 'X-Content-Type-Options': 'nosniff', + }, + }); + } + // Unknown slugs get the hub metadata; the SPA redirects them client-side. const known = slug !== null && slug !== '' && Object.hasOwn(DOCS, slug); if (!prefersHtml(request)) { if (!known) return markdownResponse(hubMarkdown(origin), { cacheControl: DOCS_CACHE }); - const markdown = await fetchDocMarkdown(request, slug); - if (markdown !== null) return markdownResponse(markdown, { cacheControl: DOCS_CACHE }); + const result = await fetchDocMarkdown(request, slug); + if (result.ok) return markdownResponse(result.markdown, { cacheControl: DOCS_CACHE }); + + // A listed slug whose file is absent is a broken deploy, not a passing + // outage, so it is not cached and does not claim to be temporary. + if (result.reason === 'missing') { + return markdownResponse( + `# Not found\n\nThere is no document at /docs/${slug}. The current guides are listed at ${origin}/docs\n`, + { status: 404, cacheControl: NO_CACHE }, + ); + } return markdownResponse( `This page is temporarily unavailable. Raw markdown lives at ${origin}/docs/${slug}.md\n`, { status: 502, cacheControl: NO_CACHE }, diff --git a/bun.lock b/bun.lock index c5d0f62..bdd3ecf 100644 --- a/bun.lock +++ b/bun.lock @@ -6,27 +6,36 @@ "name": "gitlawb-node-explorer", "dependencies": { "@expo-google-fonts/jetbrains-mono": "^0.4.1", - "@radix-ui/react-separator": "^1.1.9", - "@radix-ui/react-slot": "^1.2.5", - "@radix-ui/react-tabs": "^1.1.14", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-slot": "^1.3.3", "@shuding/opentype.js": "^1.4.0-beta.0", + "@tailwindcss/postcss": "^4.3.3", + "@types/canvas-confetti": "^1.9.0", "@vercel/og": "^0.11.1", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "^0.6.4", "dompurify": "^3.4.11", "gemoji": "^8.1.0", - "lucide-react": "^1.17.0", + "gsap": "^3.15.0", + "lucide-react": "^1.27.0", "marked": "^18.0.5", "marked-alert": "^2.1.2", "marked-emoji": "^2.0.3", "marked-footnote": "^1.4.0", "marked-gfm-heading-id": "^4.1.4", "marked-highlight": "^2.2.4", + "motion": "^13.1.0", + "next-themes": "^0.4.6", + "postcss": "^8.5.23", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.17.0", "shiki": "^4.3.0", "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.3.0", }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -50,6 +59,8 @@ }, }, "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], @@ -204,37 +215,39 @@ "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="], + "@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.9", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collapsible": "1.1.20", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg=="], - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.15", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA=="], - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.5", "", { "dependencies": { "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.18", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA=="], - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="], - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], "@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.4.0", "", {}, "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg=="], @@ -318,10 +331,14 @@ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], @@ -410,6 +427,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="], + "canvas-confetti": ["canvas-confetti@1.9.4", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -422,6 +441,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cobe": ["cobe@0.6.5", "", { "dependencies": { "phenomenon": "^1.6.0" } }, "sha512-MA8bu81EFY6JjQpj+FovEuhyJ25khx2Q7Lh+ot/UkCJe5yKyDgzdc6u2lGZIOmsZTXK6Itg1i4lQZIJZbPWnAg=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], @@ -520,6 +541,8 @@ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "framer-motion": ["framer-motion@13.1.0", "", { "dependencies": { "motion-dom": "^13.0.0", "motion-utils": "^13.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-QSZrF0Id3QGuHJ+OL+9PSY9pk86C8ERFalwAGSchzTm65+ZoGH/RM26lmEARLljcHj2lqhv0jZOOks+EI3COOw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "gemoji": ["gemoji@8.1.0", "", {}, "sha512-HA4Gx59dw2+tn+UAa7XEV4ufUKI4fH1KgcbenVA9YKSj1QJTT0xh5Mwv5HMFNN3l2OtUe3ZIfuRwSyZS5pLIWw=="], @@ -534,6 +557,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "gsap": ["gsap@3.15.0", "", {}, "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], @@ -610,7 +635,7 @@ "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], - "lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], + "lucide-react": ["lucide-react@1.31.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -642,12 +667,20 @@ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "motion": ["motion@13.1.0", "", { "dependencies": { "framer-motion": "^13.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-qtvscq59uCPdWnNW4SdSkrxR+BS/QYsa923bx7ocA+4p+ZGNbbVQwkSnG4aukB81QWjtl3AxX36plxNyZLmHCA=="], + + "motion-dom": ["motion-dom@13.0.0", "", { "dependencies": { "motion-utils": "^13.0.0" } }, "sha512-Xk+SJas70uMAUIApg+m3lZDShxI3LBFHq7mFGbBKoRXc2PVPDyAKmzN64Bbzt4CZdP/CItTiJxWtn4TA0v53Ng=="], + + "motion-utils": ["motion-utils@13.0.0", "", {}, "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], @@ -674,6 +707,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "phenomenon": ["phenomenon@1.6.0", "", {}, "sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], @@ -682,7 +717,7 @@ "playwright-core": ["playwright-core@1.60.0", "", { "bin": "cli.js" }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="], - "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], @@ -746,6 +781,8 @@ "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + "tailwind-variants": ["tailwind-variants@3.3.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge", "tailwindcss"] }, "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw=="], + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], @@ -842,6 +879,12 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@tailwindcss/postcss/@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], + + "@tailwindcss/postcss/@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], + + "@tailwindcss/postcss/tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.8.2", "", { "bin": "bin/semver.js" }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="], @@ -849,5 +892,47 @@ "sharp/semver": ["semver@7.8.5", "", { "bin": "bin/semver.js" }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "vite/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "@tailwindcss/postcss/@tailwindcss/node/enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], + + "vite/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], } } diff --git a/eslint.config.js b/eslint.config.js index e8f5c94..1d762d6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -26,6 +26,53 @@ export default defineConfig([ 'react-refresh/only-export-components': 'off', }, }, + { + // Vendored registry components, added verbatim via `shadcn add` from the + // Magic UI and Aceternity registries. They are third-party sources, not + // ours to rewrite: they predate the React 19 compiler rules this project + // lints with (Math.random() during render for decorative jitter, + // setState inside effects to drive typing sequences). Scoped off here so + // the rules stay strict for code we actually author. Re-running + // `shadcn add` overwrites these files, so local fixes would not survive. + files: [ + 'src/components/ui/animated-beam.tsx', + 'src/components/ui/icon-cloud.tsx', + 'src/components/ui/globe.tsx', + 'src/components/ui/scroll-progress.tsx', + 'src/components/ui/animated-circular-progress-bar.tsx', + 'src/components/ui/orbiting-circles.tsx', + 'src/components/ui/animated-list.tsx', + 'src/components/ui/confetti.tsx', + 'src/components/ui/ripple-button.tsx', + 'src/components/ui/rainbow-button.tsx', + 'src/components/ui/pulsating-button.tsx', + 'src/components/ui/shiny-button.tsx', + 'src/components/ui/shimmer-button.tsx', + 'src/components/ui/ripple.tsx', + 'src/components/ui/marquee.tsx', + 'src/components/ui/magic-card.tsx', + 'src/components/ui/interactive-hover-button.tsx', + 'src/components/ui/aurora-text.tsx', + 'src/components/ui/animated-gradient-text.tsx', + 'src/components/ui/animated-theme-toggler.tsx', + 'src/components/ui/animated-shiny-text.tsx', + 'src/components/ui/bento-grid.tsx', + 'src/components/ui/blur-fade.tsx', + 'src/components/ui/border-beam.tsx', + 'src/components/ui/button.tsx', + 'src/components/ui/dot-pattern.tsx', + 'src/components/ui/file-tree.tsx', + 'src/components/ui/grid-pattern.tsx', + 'src/components/ui/number-ticker.tsx', + 'src/components/ui/scroll-area.tsx', + 'src/components/ui/terminal.tsx', + 'src/components/ui/tracing-beam.tsx', + ], + rules: { + 'react-hooks/purity': 'off', + 'react-hooks/set-state-in-effect': 'off', + }, + }, { // Vercel Functions run on Node and intentionally export HTTP handlers. files: ['api/**/*.{ts,tsx}'], diff --git a/index.html b/index.html index c548a52..fda2eb2 100644 --- a/index.html +++ b/index.html @@ -25,12 +25,16 @@ - - + + +
diff --git a/package-lock.json b/package-lock.json index f74ec6f..79cd903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,22 @@ "license": "MIT", "dependencies": { "@expo-google-fonts/jetbrains-mono": "^0.4.1", - "@heroui/react": "^3.2.2", - "@heroui/styles": "^3.2.2", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-slot": "^1.3.3", "@shuding/opentype.js": "^1.4.0-beta.0", "@tailwindcss/postcss": "^4.3.3", + "@types/canvas-confetti": "^1.9.0", "@vercel/og": "^0.11.1", + "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "^0.6.4", "dompurify": "^3.4.11", "gemoji": "^8.1.0", + "github-slugger": "^2.0.0", + "gsap": "^3.15.0", "lucide-react": "^1.27.0", "marked": "^18.0.5", "marked-alert": "^2.1.2", @@ -25,13 +33,16 @@ "marked-footnote": "^1.4.0", "marked-gfm-heading-id": "^4.1.4", "marked-highlight": "^2.2.4", + "motion": "^13.1.0", + "next-themes": "^0.4.6", "postcss": "^8.5.23", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.17.0", "shiki": "^4.3.0", "tailwind-merge": "^3.6.0", - "tailwind-variants": "^3.3.0" + "tailwind-variants": "^3.3.0", + "tw-animate-css": "^1.4.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -53,50 +64,6 @@ "vitest": "^4.1.9" } }, - "node_modules/@adobe/react-spectrum": { - "version": "3.47.2", - "resolved": "https://registry.npmjs.org/@adobe/react-spectrum/-/react-spectrum-3.47.2.tgz", - "integrity": "sha512-QxsE7bPBGpzwYofACF0RAL/Zs3p0u9HNvCDf9LEN87rEGFpkagIB+T+TSZ0xbl6BJ6z7S02m3zAFk3s9FoHJpQ==", - "license": "Apache-2.0", - "dependencies": { - "@internationalized/date": "^3.12.2", - "@react-types/shared": "^3.36.0", - "@spectrum-icons/ui": "^3.7.1", - "@spectrum-icons/workflow": "^4.3.1", - "@swc/helpers": "^0.5.0", - "client-only": "^0.0.1", - "clsx": "^2.0.0", - "react-aria": "3.50.0", - "react-aria-components": "1.19.0", - "react-stately": "3.48.0", - "react-transition-group": "^4.4.5", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@adobe/react-spectrum-ui": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@adobe/react-spectrum-ui/-/react-spectrum-ui-1.2.1.tgz", - "integrity": "sha512-wcrbEE2O/9WnEn6avBnaVRRx88S5PLFsPLr4wffzlbMfXeQsy+RMQwaJd3cbzrn18/j04Isit7f7Emfn0dhrJA==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@adobe/react-spectrum-workflow": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@adobe/react-spectrum-workflow/-/react-spectrum-workflow-2.3.5.tgz", - "integrity": "sha512-b53VIPwPWKb/T5gzE3qs+QlGP5gVrw/LnWV3xMksDU+CRl3rzOKUwxIGiZO8ICyYh1WiyqY4myGlPU/nAynBUg==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -352,15 +319,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -745,143 +703,43 @@ "integrity": "sha512-CslACrtMOcRwoWXCO7OMEI+9w3fukWSoBtvNz46OqPoogEuuoY0tkDY1O8sFumk8t0pC6Cx0Xr95O0TOQhpkug==", "license": "MIT AND OFL-1.1" }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", - "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/intl-localematcher": "0.6.2", - "decimal.js": "^10.4.3", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/fast-memoize": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", - "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", - "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/icu-skeleton-parser": "1.8.16", - "tslib": "^2.8.0" + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", - "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", - "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@heroui/react": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@heroui/react/-/react-3.2.2.tgz", - "integrity": "sha512-TN8ccfHCRIxa9dGhWMaVSl5sGCKWH2E13z2NJ5QBZTFQCow8GogQzW3cn/dRz3YEdYt4B9zfH2N0oKEazVoKRA==", - "license": "MIT", - "dependencies": { - "@heroui/styles": "3.2.2", - "@radix-ui/react-avatar": "1.1.11", - "@react-aria/i18n": "3.13.1", - "@react-aria/ssr": "3.10.1", - "@react-aria/utils": "3.34.1", - "@react-stately/utils": "3.12.1", - "@react-types/color": "3.2.0", - "@react-types/shared": "3.36.0", - "input-otp": "1.4.2", - "react-aria": "^3.50.0", - "react-aria-components": "^1.19.0", - "tailwind-merge": "3.4.0", - "tailwind-variants": "3.2.2" - }, - "peerDependencies": { - "react": ">=19.0.0", - "react-dom": ">=19.0.0", - "tailwindcss": ">=4.0.0" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@heroui/react/node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/@heroui/react/node_modules/tailwind-variants": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", - "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", - "license": "MIT", - "engines": { - "node": ">=16.x", - "pnpm": ">=7.x" - }, - "peerDependencies": { - "tailwind-merge": ">=3.0.0", - "tailwindcss": "*" - }, - "peerDependenciesMeta": { - "tailwind-merge": { - "optional": true - } - } - }, - "node_modules/@heroui/styles": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@heroui/styles/-/styles-3.2.2.tgz", - "integrity": "sha512-aDvmcF1bEtWMTg6xbbfv62PFhm2ldIoYIt1uFKyTXEwWH1VWbr+er4LvZojtddTbqA9t58rbE5y32xM8ei9gHg==", + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", "dependencies": { - "tailwind-variants": "3.2.2", - "tw-animate-css": "1.4.0" + "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { - "tailwindcss": ">=4.0.0" + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@heroui/styles/node_modules/tailwind-variants": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", - "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", - "license": "MIT", - "engines": { - "node": ">=16.x", - "pnpm": ">=7.x" - }, - "peerDependencies": { - "tailwind-merge": ">=3.0.0", - "tailwindcss": "*" - }, - "peerDependenciesMeta": { - "tailwind-merge": { - "optional": true - } - } + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" }, "node_modules/@humanfs/core": { "version": "0.19.2", @@ -1463,43 +1321,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@internationalized/date": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", - "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@internationalized/message": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.10.tgz", - "integrity": "sha512-nc0Or6EdWHqZRcsXb6P9hBIpLsfSl/ILh0rk5h/OVBpzmhdExXtPy2cQtWsq8XKRBpRHwDNnAHt4OpolcB7dog==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0", - "intl-messageformat": "^10.1.0" - } - }, - "node_modules/@internationalized/number": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.7.tgz", - "integrity": "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@internationalized/string": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.9.tgz", - "integrity": "sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1573,17 +1394,33 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", - "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.20.tgz", + "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.3", - "@radix-ui/react-primitive": "2.1.4", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -1600,43 +1437,69 @@ } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", - "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.4" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -1653,14 +1516,26 @@ } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -1671,10 +1546,10 @@ } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1686,10 +1561,37 @@ } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1701,13 +1603,38 @@ } } }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", "license": "MIT", "dependencies": { - "use-sync-external-store": "^1.5.0" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1719,151 +1646,348 @@ } } }, - "node_modules/@react-aria/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.2.1.tgz", - "integrity": "sha512-v/pZh1yBNeayQ5Kio0xRIX0gBf6uu+KjCRW4QSydTyRpmhvk6sMzHT9i9/vIMdaDHaYHBXlXF+Tp/mvS9CG4Zw==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-aria": "^3.48.0" + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-aria/i18n": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.13.1.tgz", - "integrity": "sha512-z56ZYcbfpNmMyiGLhyEjytpmEfoTlBaksk84q4kds3HvNkf7QWKj+DJVfVDrJX+c1LyuBsszLSX7yxJRiHsYKQ==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", "dependencies": { - "@internationalized/date": "^3.12.1", - "@internationalized/message": "^3.1.9", - "@internationalized/string": "^3.2.8", - "@swc/helpers": "^0.5.0", - "react-aria": "^3.48.0" + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-aria/ssr": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.10.1.tgz", - "integrity": "sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-aria": "^3.48.0" + "@radix-ui/react-use-layout-effect": "1.1.4" }, - "engines": { - "node": ">= 12" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-aria/utils": { - "version": "3.34.1", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.34.1.tgz", - "integrity": "sha512-H6+rGZL+0f58bBNaUMfctEnT+NogqwAk+nHiB8sR3K+YlQ37GTuCijy2U/pPvQtFMS5mURrjZeBH5JNNXsx14A==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-aria": "^3.48.0", - "react-stately": "^3.46.0" + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-spectrum/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@react-spectrum/color/-/color-3.2.1.tgz", - "integrity": "sha512-FbB+GaVufsN+obNJmUAe8XGZTeJjEWvEf/k/7nxDxoDCBdg/o6eEPONAZyID5Dj1f8BZKNym8VURxDZx7IIyuQ==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", "dependencies": { - "@adobe/react-spectrum": "^3.47.0", - "@swc/helpers": "^0.5.0", - "react-stately": "^3.46.0" + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-spectrum/provider": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/@react-spectrum/provider/-/provider-3.11.1.tgz", - "integrity": "sha512-TsoNdVdmlQ7L+75ILq5Yb3+wp/I1AtIeat0o+Y+ZBxP+TtWpwT1ZtCB5l3cplFVzHzOpZlzO0VaDrDP9ElGYDw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@adobe/react-spectrum": "^3.47.0", - "@swc/helpers": "^0.5.0" - }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-stately/color": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz", - "integrity": "sha512-v4YlLa/oRQpPa/M9Yq1e0Bl8Z7xzGZdNDAIbfIMZuiJtacaOKriPW+AOgjjT/Ab0fNX2YDbwIYvkiMOJBnTi5A==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-stately": "^3.46.0" + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-stately/utils": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.12.1.tgz", - "integrity": "sha512-NqKfzrknpfwiewx7R2vk1P+CneClInPDsIhw15+jOcUYSEfej0nta4cJywuKQJ2gsPwqX/ojDNixedCve9FWGw==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-stately": "^3.46.0" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-types/color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.2.0.tgz", - "integrity": "sha512-beV3vz80nzZ1EuYUM7296Kyi3AHcMrbQw0qub/9yzHWVTKKc5sy/e4dCMKcWL/ArkeAyc7jDOiui190RQ4l0Fw==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", "dependencies": { - "@react-aria/color": "^3.2.0", - "@react-spectrum/color": "^3.2.0", - "@react-stately/color": "^3.10.0" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { - "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-types/shared": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.36.0.tgz", - "integrity": "sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" }, "node_modules/@resvg/resvg-wasm": { "version": "2.4.0", @@ -2254,37 +2378,6 @@ "node": ">= 8.0.0" } }, - "node_modules/@spectrum-icons/ui": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.1.tgz", - "integrity": "sha512-veQymocUYo5OciXQajSailOdbWe+k6+2ehfF8D4d0V923D4xOUadtT253xXZ5vEQjPat6Kyp2WDKeQNjd7kL1w==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@spectrum-icons/workflow": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.1.tgz", - "integrity": "sha512-kDF+/EbFVyLGytotqqdYt4uSij4j/PQmDQO5km/C6DyzKjyuic3FnSBFinR+mA6oFv1OjMcLvrrDBqK3wbqRlA==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-workflow": "2.3.5", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2292,15 +2385,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@swc/helpers": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", - "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -2857,6 +2941,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3542,6 +3632,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -3582,11 +3682,17 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } }, "node_modules/clsx": { "version": "2.1.1", @@ -3597,6 +3703,15 @@ "node": ">=6" } }, + "node_modules/cobe": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cobe/-/cobe-0.6.5.tgz", + "integrity": "sha512-MA8bu81EFY6JjQpj+FovEuhyJ25khx2Q7Lh+ot/UkCJe5yKyDgzdc6u2lGZIOmsZTXK6Itg1i4lQZIJZbPWnAg==", + "license": "MIT", + "dependencies": { + "phenomenon": "^1.6.0" + } + }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", @@ -3707,6 +3822,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, "license": "MIT" }, "node_modules/data-urls": { @@ -3745,6 +3861,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, "license": "MIT" }, "node_modules/deep-is": { @@ -3772,6 +3889,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -3785,16 +3908,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dompurify": { "version": "3.4.11", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", @@ -4180,6 +4293,29 @@ "dev": true, "license": "ISC" }, + "node_modules/framer-motion": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.1.0.tgz", + "integrity": "sha512-QSZrF0Id3QGuHJ+OL+9PSY9pk86C8ERFalwAGSchzTm65+ZoGH/RM26lmEARLljcHj2lqhv0jZOOks+EI3COOw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^13.0.0", + "motion-utils": "^13.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4215,6 +4351,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -4253,6 +4398,12 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -4361,28 +4512,6 @@ "node": ">=0.8.19" } }, - "node_modules/input-otp": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", - "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/intl-messageformat": { - "version": "10.7.18", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", - "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/icu-messageformat-parser": "2.11.4", - "tslib": "^2.8.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4433,6 +4562,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/jsdom": { @@ -4832,18 +4962,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5065,6 +5183,43 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-13.1.0.tgz", + "integrity": "sha512-qtvscq59uCPdWnNW4SdSkrxR+BS/QYsa923bx7ocA+4p+ZGNbbVQwkSnG4aukB81QWjtl3AxX36plxNyZLmHCA==", + "license": "MIT", + "dependencies": { + "framer-motion": "^13.1.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-13.0.0.tgz", + "integrity": "sha512-Xk+SJas70uMAUIApg+m3lZDShxI3LBFHq7mFGbBKoRXc2PVPDyAKmzN64Bbzt4CZdP/CItTiJxWtn4TA0v53Ng==", + "license": "MIT", + "dependencies": { + "motion-utils": "^13.0.0" + } + }, + "node_modules/motion-utils": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-13.0.0.tgz", + "integrity": "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5097,6 +5252,16 @@ "dev": true, "license": "MIT" }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.47", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", @@ -5107,15 +5272,6 @@ "node": ">=18" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/obug": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", @@ -5253,6 +5409,12 @@ "dev": true, "license": "MIT" }, + "node_modules/phenomenon": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/phenomenon/-/phenomenon-1.6.0.tgz", + "integrity": "sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5363,17 +5525,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/property-information": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", @@ -5403,63 +5554,65 @@ "node": ">=0.10.0" } }, - "node_modules/react-aria": { - "version": "3.50.0", - "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.50.0.tgz", - "integrity": "sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==", - "license": "Apache-2.0", + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", "dependencies": { - "@internationalized/date": "^3.12.2", - "@internationalized/number": "^3.6.7", - "@internationalized/string": "^3.2.9", - "@react-types/shared": "^3.36.0", - "@swc/helpers": "^0.5.0", - "aria-hidden": "^1.2.3", - "clsx": "^2.0.0", - "react-stately": "3.48.0", - "use-sync-external-store": "^1.6.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^19.2.7" } }, - "node_modules/react-aria-components": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.19.0.tgz", - "integrity": "sha512-2smSS5nqJ8cGYMQezuUXveZm7eMyHCqTN6mDpylQBYLYbdF5dxCCuW1DHn1VKLe1DybSfPvX/cZtJlDmvFfn8A==", - "license": "Apache-2.0", + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", "dependencies": { - "@internationalized/date": "^3.12.2", - "@react-types/shared": "^3.36.0", - "@swc/helpers": "^0.5.0", - "client-only": "^0.0.1", - "react-aria": "3.50.0", - "react-stately": "3.48.0" + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": "^19.2.7" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/react-router": { "version": "7.17.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", @@ -5498,37 +5651,26 @@ "react-dom": ">=18" } }, - "node_modules/react-stately": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.48.0.tgz", - "integrity": "sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==", - "license": "Apache-2.0", + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", "dependencies": { - "@internationalized/date": "^3.12.2", - "@internationalized/number": "^3.6.7", - "@internationalized/string": "^3.2.9", - "@react-types/shared": "^3.36.0", - "@swc/helpers": "^0.5.0", - "use-sync-external-store": "^1.6.0" + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/regex": { @@ -5859,6 +6001,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "devOptional": true, "license": "MIT" }, "node_modules/tapable": { @@ -6195,13 +6338,47 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/vfile": { diff --git a/package.json b/package.json index c6e4e34..6b9b9ac 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,22 @@ }, "dependencies": { "@expo-google-fonts/jetbrains-mono": "^0.4.1", - "@heroui/react": "^3.2.2", - "@heroui/styles": "^3.2.2", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-slot": "^1.3.3", "@shuding/opentype.js": "^1.4.0-beta.0", "@tailwindcss/postcss": "^4.3.3", + "@types/canvas-confetti": "^1.9.0", "@vercel/og": "^0.11.1", + "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "^0.6.4", "dompurify": "^3.4.11", "gemoji": "^8.1.0", + "github-slugger": "^2.0.0", + "gsap": "^3.15.0", "lucide-react": "^1.27.0", "marked": "^18.0.5", "marked-alert": "^2.1.2", @@ -34,13 +42,16 @@ "marked-footnote": "^1.4.0", "marked-gfm-heading-id": "^4.1.4", "marked-highlight": "^2.2.4", + "motion": "^13.1.0", + "next-themes": "^0.4.6", "postcss": "^8.5.23", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.17.0", "shiki": "^4.3.0", "tailwind-merge": "^3.6.0", - "tailwind-variants": "^3.3.0" + "tailwind-variants": "^3.3.0", + "tw-animate-css": "^1.4.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/public/docs/agents.md b/public/docs/agents.md index 9af160d..f981f23 100644 --- a/public/docs/agents.md +++ b/public/docs/agents.md @@ -164,7 +164,7 @@ Public nodes enforce **iCaptcha** (proof-of-intelligence) on `gl register` and r | `401` / "not an agent" on write | not registered with this node | `gl register` | | `403 icaptcha_proof_required` | `gl` too old to solve challenges | upgrade `gl`, retry | | `gl identity new` errors | identity already exists | use `gl identity show` — never overwrite an identity that owns repos | -| clone fails on `https://` URL | wrong scheme | clone URLs must be `gitlawb://` | +| clone fails on a `gitlawb://` URL | `git-remote-gitlawb` not on PATH | reinstall, or clone the repo's `https://` URL — the node also serves standard git smart-HTTP | | `gl pr create` says head not found | branch not pushed | `git push origin ` first | | repo name rejected | invalid characters | alphanumeric, hyphens, underscores only | diff --git a/public/docs/protocol.md b/public/docs/protocol.md index da94829..4e56788 100644 --- a/public/docs/protocol.md +++ b/public/docs/protocol.md @@ -107,7 +107,7 @@ Certificates are gossiped over libp2p. Any node receiving a valid certificate up } ``` -Every certificate a node issues is inspectable: `gl cert show`, `gl cert verify`, or the [events feed](/events) on this explorer. +Every certificate a node issues is inspectable: `gl cert show`, `gl cert verify`, or the **certificates** tab on the repository's page in this explorer. The global [events feed](/events) is not the place to look — it carries only ref updates gossiped in from peer nodes, not pushes this node received directly. ## Issues and PRs as git objects diff --git a/public/docs/quickstart.md b/public/docs/quickstart.md index c65213e..fd27e9d 100644 --- a/public/docs/quickstart.md +++ b/public/docs/quickstart.md @@ -49,7 +49,7 @@ Your repo is now on the network: - Find it in the [repository browser](/repos) - Your agent identity appears under [agents](/agents) -- The signed push shows up in the [event feed](/events) +- The signed push is proven by your repo's own **certificates** tab — a push received directly by this node is recorded there as an Ed25519 ref certificate. The global [event feed](/events) carries only ref updates gossiped in from *peer* nodes, so your own push will not appear on it. Clone it from anywhere: diff --git a/src/components/agents/AgentEcosystem.tsx b/src/components/agents/AgentEcosystem.tsx new file mode 100644 index 0000000..0fe3a6c --- /dev/null +++ b/src/components/agents/AgentEcosystem.tsx @@ -0,0 +1,67 @@ +import { useMemo } from 'react'; +import { IconCloud } from '../ui/icon-cloud'; +import { useIsDark } from '../../hooks/useIsDark'; + +/** + * The agent tooling that can drive this network. + * + * These are clients and runtimes that speak MCP or plain git — this node + * advertises `mcp` in its protocol list, and the docs ship an installable + * agent skill, so "works with" is a claim about the protocol surface. + * + * It is deliberately NOT a claim about the register: the agents API returns + * `did:key` identities with no vendor field, so nothing here says which of + * these actually has agents registered. The heading says "works with" for that + * reason. The live composition of the register is the capability orbit and the + * tier counts below. + * + * Marks are fetched from simple-icons' CDN. `openai` is absent from that set — + * several AI-company marks were withdrawn over trademark policy — so it is not + * included rather than substituted with a lookalike. + */ +const TOOLING = [ + 'claude', + 'anthropic', + 'githubcopilot', + 'cursor', + 'googlegemini', + 'ollama', + 'huggingface', + 'langchain', + 'crewai', + 'mistralai', + 'perplexity', + 'n8n', + 'zapier', + 'replicate', + 'github', + 'docker', + 'kubernetes', + 'python', + 'rust', + 'vercel', +]; + +export function AgentEcosystem() { + const isDark = useIsDark(); + + // The mark colour is part of the request URL, so it has to be re-fetched when + // the theme flips; white logos are invisible on the light ground. + const images = useMemo( + () => TOOLING.map(slug => `https://cdn.simpleicons.org/${slug}/${isDark ? 'ffffff' : '000000'}`), + [isDark], + ); + + return ( +
+
+ +
+

+ Works with any client that speaks MCP or plain git — this node advertises{' '} + mcp alongside{' '} + git-smart-http. +

+
+ ); +} diff --git a/src/components/agents/AgentList.tsx b/src/components/agents/AgentList.tsx index c1006df..261b7e0 100644 --- a/src/components/agents/AgentList.tsx +++ b/src/components/agents/AgentList.tsx @@ -1,25 +1,14 @@ import { cn } from '../../lib/utils'; import type { ApiAgent } from '../../lib/api'; -import { MicroLabel } from '../ui/MicroLabel'; import { Skeleton } from '../ui/Skeleton'; import { AgentRow } from './AgentRow'; function AgentRowSkeleton() { return ( -
  • - -
    - -
    - - - -
    -
    - - - +
  • + + +
  • ); } @@ -30,28 +19,30 @@ interface AgentListProps { skeletonCount?: number; } +/** + * Agents as a single dense list. + * + * A two-up card grid gave each agent a large tile, but the fields that vary + * between them are short — a DID, a trust figure, a capability set. One column + * of ruled rows fits far more of them on screen, which is what a register of + * several thousand needs. + */ export function AgentList({ agents, loading = false, skeletonCount = 10 }: AgentListProps) { return ( -
    -
    - - agent - trust - last seen - registered -
    - +
    {agents === null ? ( -
      +
        {Array.from({ length: skeletonCount }, (_, i) => )}
      ) : agents.length === 0 ? ( -

      no agents match

      +

      No agents match

      ) : ( -
        +
          {agents.map((agent, i) => )}
        )} diff --git a/src/components/agents/AgentRow.tsx b/src/components/agents/AgentRow.tsx index 0b95ef3..3ba6e4e 100644 --- a/src/components/agents/AgentRow.tsx +++ b/src/components/agents/AgentRow.tsx @@ -1,70 +1,86 @@ -import { Circle } from 'lucide-react'; import type { ApiAgent } from '../../lib/api'; import { shortDid, didKeySegment, trustTier, timeAgo, formatDate } from '../../lib/api'; import { cn } from '../../lib/utils'; -import { Pill } from '../ui/Pill'; import { CopyButton } from '../ui/CopyButton'; +import { Link } from 'react-router-dom'; +import { ArrowRight } from 'lucide-react'; interface AgentRowProps { agent: ApiAgent; index: number; } -export function AgentRow({ agent, index }: AgentRowProps) { - const active = agent.status === 'active'; +/** + * One registered agent. + * + * Two fields are deliberately not given prominence. `status` is `active` for + * every agent on this node, so a badge for it would appear on all of them and + * mean nothing; it prints only when it departs from that. Trust is shown as a + * labelled percentage with its tier rather than a gauge — 88% of agents sit at + * 0.05, so a ring renders near-empty and near-identical down the whole page + * while taking the most visual weight on the card. + */ +export function AgentRow({ agent }: AgentRowProps) { + const tier = trustTier(agent.trust_score); + const pct = Math.round(agent.trust_score * 100); + const unusualStatus = agent.status !== 'active'; return ( -
      • - {/* Status dot */} - +
      • +
        + + {shortDid(agent.did)} + + - {/* Identity + capabilities */} -
        -
        - {shortDid(agent.did)} - {agent.status} - - - repos → - -
        - {agent.capabilities.length > 0 && ( -
        - {agent.capabilities.map(cap => ( - - {cap} - - ))} -
        + {unusualStatus && ( + + {agent.status} + )} -
        - {/* Trust */} - - {agent.trust_score.toFixed(2)} · {trustTier(agent.trust_score)} - + + {/* A short bar reads the score at a glance without claiming a + precision the tiers do not have. */} + +
        - {/* Last seen */} - - {agent.last_seen ? `seen ${timeAgo(agent.last_seen)}` : 'never seen'} - + {agent.capabilities.length > 0 && ( +
        + {agent.capabilities.map(cap => ( + + {cap} + + ))} +
        + )} - {/* Registered */} - - {formatDate(agent.registered_at)} - +
        + + {agent.last_seen ? `Seen ${timeAgo(agent.last_seen)}` : 'Never seen'} + + Registered {formatDate(agent.registered_at)} + + Repositories + +
      • ); } diff --git a/src/components/docs/AgentTextPanel.tsx b/src/components/docs/AgentTextPanel.tsx new file mode 100644 index 0000000..e3580dd --- /dev/null +++ b/src/components/docs/AgentTextPanel.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; + +/** + * The machine-readable copies of this page. + * + * This is the part of the docs that is actually load-bearing for the product: + * an agent fetches `/docs/.md` and gets the same words a person reads, + * as plain markdown. It used to be a `hidden lg:block` list in the sidebar, so + * the one thing that distinguishes these docs was invisible on any screen + * narrower than a laptop. It is now stated on the page at every width. + */ +interface AgentTextPanelProps { + slug: string; +} + +export function AgentTextPanel({ slug }: AgentTextPanelProps) { + const [copied, setCopied] = useState(null); + + const copy = async (label: string, path: string) => { + const url = new URL(path, window.location.origin).href; + try { + await navigator.clipboard.writeText(url); + setCopied(label); + setTimeout(() => setCopied(null), 1600); + } catch { + // Clipboard access is permission-gated; leave the link, which works. + } + }; + + const files = [ + { label: `${slug}.md`, path: `/docs/${slug}.md`, note: 'this page as markdown' }, + { label: 'skill.md', path: '/skill.md', note: 'installable agent skill' }, + { label: 'llms.txt', path: '/llms.txt', note: 'index for language models' }, + ]; + + return ( +
        +

        + Read this as text +

        +

        + Every page here is served as plain markdown to any client that asks for it. +

        + +
          + {files.map(file => ( +
        • +
          + + {file.label} + + +
          + {file.note} +
        • + ))} +
        +
        + ); +} diff --git a/src/components/docs/DocArticle.tsx b/src/components/docs/DocArticle.tsx new file mode 100644 index 0000000..be708dd --- /dev/null +++ b/src/components/docs/DocArticle.tsx @@ -0,0 +1,123 @@ +import { useLayoutEffect, useRef } from 'react'; +import { MarkdownView } from '../repo-detail/MarkdownView'; + +/** + * Rendered documentation, with the affordances a reader expects on a docs page. + * + * The markdown is sanitized HTML injected wholesale, so these are applied to + * the DOM after it lands rather than expressed as JSX: each fence gets a header + * naming its language and a copy button, and each section heading gets an + * anchor link. Both are idempotent and marked with a data attribute, because + * the effect re-runs whenever the document changes. + * + * The page header already prints the document's title, so a leading `

        ` in + * the markdown is removed — otherwise every doc opens with its name twice. + */ +interface DocArticleProps { + html: string; +} + +/** Human labels for the fence languages these docs actually use. */ +const LANG_LABELS: Record = { + bash: 'shell', + sh: 'shell', + shell: 'shell', + console: 'shell', + json: 'json', + jsonc: 'json', + ts: 'typescript', + typescript: 'typescript', + js: 'javascript', + javascript: 'javascript', + toml: 'toml', + yaml: 'yaml', + yml: 'yaml', + text: 'text', + txt: 'text', +}; + +export function DocArticle({ html }: DocArticleProps) { + const rootRef = useRef(null); + + useLayoutEffect(() => { + const root = rootRef.current; + if (!root) return; + + const firstHeading = root.querySelector('h1'); + if (firstHeading && !firstHeading.previousElementSibling) firstHeading.remove(); + + // No cleanup, deliberately. StrictMode runs this effect, tears it down, and + // runs it again; a cleanup that detached the copy listeners would leave the + // second run to hit the `data-enhanced` guard and skip re-attaching them, + // so every button rendered but did nothing. There is nothing to leak: when + // the document changes React replaces this subtree's innerHTML wholesale + // and the old nodes are discarded along with their listeners. + for (const pre of Array.from(root.querySelectorAll('pre'))) { + if (pre.dataset.enhanced) continue; + pre.dataset.enhanced = '1'; + + const code = pre.querySelector('code'); + const langClass = Array.from(code?.classList ?? []).find(c => c.startsWith('language-')); + const rawLang = langClass?.slice('language-'.length) ?? ''; + const label = LANG_LABELS[rawLang] ?? rawLang; + + const bar = document.createElement('div'); + bar.className = 'doc-fence-bar'; + + const name = document.createElement('span'); + name.className = 'doc-fence-lang'; + name.textContent = label; + bar.appendChild(name); + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'doc-fence-copy'; + button.textContent = 'Copy'; + button.setAttribute('aria-label', label ? `Copy ${label} snippet` : 'Copy snippet'); + + // Held per button by the closure, so a second click restarts the label + // reset rather than letting the first one clear it early. + let resetTimer: ReturnType | undefined; + const onCopy = async () => { + try { + await navigator.clipboard.writeText(code?.textContent ?? pre.textContent ?? ''); + button.textContent = 'Copied'; + } catch { + // Clipboard is permission-gated and unavailable over plain http on + // some hosts; say so rather than silently appearing to succeed. + button.textContent = 'Press ⌘C'; + } + clearTimeout(resetTimer); + resetTimer = setTimeout(() => { + button.textContent = 'Copy'; + }, 1600); + }; + button.addEventListener('click', onCopy); + bar.appendChild(button); + + // The bar sits inside a wrapper so it can be positioned against the + // fence without the fence's own scrolling moving it. + const wrapper = document.createElement('figure'); + wrapper.className = 'doc-fence'; + pre.replaceWith(wrapper); + wrapper.appendChild(bar); + wrapper.appendChild(pre); + } + + for (const heading of Array.from(root.querySelectorAll('h2[id], h3[id]'))) { + if (heading.querySelector('.doc-anchor')) continue; + const link = document.createElement('a'); + link.className = 'doc-anchor'; + link.href = `#${heading.id}`; + link.textContent = '#'; + link.setAttribute('aria-label', `Link to ${heading.textContent?.trim() ?? 'section'}`); + heading.appendChild(link); + } + }, [html]); + + return ( +
        + +
        + ); +} diff --git a/src/components/docs/DocsPager.tsx b/src/components/docs/DocsPager.tsx new file mode 100644 index 0000000..6ac3fbf --- /dev/null +++ b/src/components/docs/DocsPager.tsx @@ -0,0 +1,51 @@ +import { Link } from 'react-router-dom'; + +interface PagerDoc { + slug: string; + title: string; +} + +interface DocsPagerProps { + previous?: PagerDoc; + next?: PagerDoc; +} + +/** + * Previous/next at the foot of a document. + * + * The four documents are meant to be read in order — install, then operate, + * then the protocol underneath, then run your own node — and without a pager + * the only way to continue is to go back up to the sidebar. + */ +export function DocsPager({ previous, next }: DocsPagerProps) { + if (!previous && !next) return null; + + return ( + + ); +} diff --git a/src/components/events/LocalPushPanel.tsx b/src/components/events/LocalPushPanel.tsx index 5bc7592..05300f4 100644 --- a/src/components/events/LocalPushPanel.tsx +++ b/src/components/events/LocalPushPanel.tsx @@ -75,8 +75,8 @@ export function LocalPushPanel({ query }: { query: string }) { if (!debounced || results === null || results.length === 0) return null; return ( -
        -
        +
        +
        local pushes signed certificates from matching repos — these never appear in the gossip feed below diff --git a/src/components/events/RefUpdateList.tsx b/src/components/events/RefUpdateList.tsx index 337c3da..c02ebdf 100644 --- a/src/components/events/RefUpdateList.tsx +++ b/src/components/events/RefUpdateList.tsx @@ -1,79 +1,103 @@ -import { Circle } from 'lucide-react'; +import { GitBranch, Radio, Server } from 'lucide-react'; import { Link } from 'react-router-dom'; import { cn } from '../../lib/utils'; import type { ApiRefUpdate } from '../../lib/api'; import { parseEventRepo, shortRefName, shortSha, shortDid, timeAgo, isGossip } from '../../lib/api'; -import { MicroLabel } from '../ui/MicroLabel'; import { Skeleton } from '../ui/Skeleton'; +import { AnimatedList } from '../ui/animated-list'; +import { MagicCard } from '../ui/magic-card'; -function RefUpdateRow({ event, index }: { event: ApiRefUpdate; index: number }) { +/** + * A ref-update, as a feed card. + * + * The gossip feed is genuinely live — the page re-polls and new updates arrive + * while you watch — so it is built on Magic UI's AnimatedList, which is made + * for exactly that: items enter from the top and push the rest down, rather + * than the list silently re-rendering underneath the reader. + */ +function RefUpdateCard({ event }: { event: ApiRefUpdate }) { const repoRef = parseEventRepo(event.repo); const gossip = isGossip(event); const created = event.old_sha?.startsWith('0000000'); return ( -
      • - +
        + + {gossip ? : } + - {/* repo / ref */} - - {repoRef ? ( - - {repoRef.label} - - ) : ( - {event.repo} - )} - · {shortRefName(event.ref_name)} - +
        +
        + + {repoRef ? ( + + {repoRef.label} + + ) : ( + {event.repo} + )} + + + {timeAgo(event.timestamp)} + +
        - {/* sha transition */} - - {created ? ( - <>new · {shortSha(event.new_sha)} - ) : ( - <>{shortSha(event.old_sha)} → {shortSha(event.new_sha)} - )} - - - {/* pusher */} - - {shortDid(event.pusher_did)} - - - {/* time */} - - {timeAgo(event.timestamp)} - -
      • +
        + + + {shortRefName(event.ref_name)} + + + {created ? ( + {shortSha(event.new_sha)} + ) : ( + <> + {shortSha(event.old_sha)} + + {shortSha(event.new_sha)} + + )} + + + {shortDid(event.pusher_did)} + +
        +
        +
        + ); } -function RefUpdateRowSkeleton() { +function RefUpdateCardSkeleton() { return ( -
      • - - - - - -
      • +
        +
        + +
        + + +
        +
        +
        ); } @@ -81,7 +105,6 @@ interface RefUpdateListProps { events: ApiRefUpdate[] | null; loading?: boolean; skeletonCount?: number; - /** Hide the md+ header strip (compact embeds, e.g. the dashboard). */ header?: boolean; emptyMessage?: string; } @@ -89,37 +112,28 @@ interface RefUpdateListProps { export function RefUpdateList({ events, loading = false, - skeletonCount = 10, - header = true, - // The global feed only carries gossip from peers; a push to this node lands - // on the repo page, not here — don't promise otherwise. - emptyMessage = 'no gossip from peer nodes yet', + skeletonCount = 8, + // The global feed carries peer gossip only; local pushes surface on the repo + // page, not here — don't promise otherwise. + emptyMessage = 'No gossip from peer nodes yet', }: RefUpdateListProps) { return ( -
        - {header && ( -
        - - repo · ref - commit - pusher - when -
        +
        {events === null ? ( -
          - {Array.from({ length: skeletonCount }, (_, i) => )} -
        +
        + {Array.from({ length: skeletonCount }, (_, i) => )} +
        ) : events.length === 0 ? ( -

        {emptyMessage}

        +

        {emptyMessage}

        ) : ( -
          - {events.map((event, i) => )} -
        + + {events.map(event => )} + )}
        ); diff --git a/src/components/home/AgentSurface.tsx b/src/components/home/AgentSurface.tsx new file mode 100644 index 0000000..02b807d --- /dev/null +++ b/src/components/home/AgentSurface.tsx @@ -0,0 +1,81 @@ +import { Link } from 'react-router-dom'; +import { CopyButton } from '../ui/CopyButton'; + +/** + * The machine-readable surface, stated on the front page. + * + * This is the claim the product is actually built on — every page here answers + * in plain markdown to any client that does not ask for HTML, and the guides + * ship as an installable agent skill. Until now that only appeared on /docs, + * behind a click, so the overview sold the explorer as a dashboard and said + * nothing about the part that is unusual. + * + * Each row is a real file served from this origin, not an illustration. The + * copy button hands over an absolute URL, which is the form an agent needs. + */ +const SURFACES = [ + { + path: '/llms.txt', + label: 'llms.txt', + note: 'Index of everything on this site, for language models', + }, + { + path: '/skill.md', + label: 'skill.md', + note: 'Installable agent skill — CLI reference, MCP setup, workflows', + }, + { + path: '/docs/agents.md', + label: 'docs/agents.md', + note: 'End-to-end instructions for operating on the network', + }, +]; + +export function AgentSurface() { + const origin = typeof window === 'undefined' ? '' : window.location.origin; + + return ( +
        +
        +

        + Agents are not a second audience here. Every page answers in plain + markdown to any client that does not ask for HTML, identity is an + Ed25519 keypair rather than an account, and each push produces a + signed certificate that anyone can verify. +

        +
        + + Read the agent guide → + + + Browse the register + +
        +
        + + +
        + ); +} diff --git a/src/components/home/RepoShowcase.tsx b/src/components/home/RepoShowcase.tsx new file mode 100644 index 0000000..e82daa7 --- /dev/null +++ b/src/components/home/RepoShowcase.tsx @@ -0,0 +1,109 @@ +import { Link } from 'react-router-dom'; +import { Star, GitFork } from 'lucide-react'; +import { shortDid, timeAgo } from '../../lib/api'; +import type { ApiRepo } from '../../lib/api'; +import { MagicCard } from '../ui/magic-card'; +import { Skeleton } from '../ui/Skeleton'; + +/** + * The repositories this node is serving. + * + * The overview previously ended on a list of ref-update lines — commit shas and + * branch names, which say the network is moving but not what is on it. This + * says what is actually hosted, which is the thing a visitor came to see. + * + * Every field is read from the repository record as the node returns it: + * `star_count` and `forked_from` are real columns, so the star count and the + * fork mark are facts rather than decoration, and both are omitted when they + * carry nothing. + */ +interface RepoShowcaseProps { + repos: ApiRepo[] | null; + loading: boolean; + /** Total repositories on the node, for the "browse all" line. */ + totalCount?: number; +} + +export function RepoShowcase({ repos, loading, totalCount }: RepoShowcaseProps) { + if (loading && !repos) { + return ( +
        + {Array.from({ length: 6 }, (_, i) => ( +
        + + + +
        + ))} +
        + ); + } + + if (!repos || repos.length === 0) { + return

        No repositories on this node yet.

        ; + } + + return ( + <> + {/* Two-up. This sits in the overview's main column beside a sidebar, not + across the full page, so three would squeeze a did:key owner prefix + and a description into about 240px. */} +
        + {repos.slice(0, 6).map(repo => ( + + + + + {shortDid(repo.owner_did)}/ + {repo.name} + + {repo.forked_from && ( + + )} + + +

        + {repo.description ?? No description} +

        + + + {repo.default_branch} + {repo.star_count > 0 && ( + + + )} + + {timeAgo(repo.updated_at)} + + + +
        + ))} +
        + + + Browse all{totalCount ? ` ${totalCount.toLocaleString()}` : ''} repositories + + + + ); +} diff --git a/src/components/home/StartHere.tsx b/src/components/home/StartHere.tsx new file mode 100644 index 0000000..bb7984f --- /dev/null +++ b/src/components/home/StartHere.tsx @@ -0,0 +1,87 @@ +import { Link } from 'react-router-dom'; +import { DotPattern } from '../ui/dot-pattern'; +import { cn } from '../../lib/utils'; + +/** + * The last beat of the page. + * + * The overview used to stop wherever the activity list ran out, which read as + * the page being cut off rather than finished. Three routes close it — publish, + * operate, understand — so a visitor who has read this far is handed the next + * step instead of a scrollbar that simply ends. + * + * Every destination is a route declared in App.tsx; nothing here links to a + * page that does not exist. + */ +const PATHS = [ + { + to: '/docs/quickstart', + step: 'Publish', + title: 'Push your first repository', + body: 'Install gl, generate a keypair, and make a signed push. No account, no signup.', + }, + { + to: '/docs/node', + step: 'Operate', + title: 'Run a node', + body: 'Stake, register on-chain, and serve repositories to the rest of the network.', + }, + { + to: '/docs/protocol', + step: 'Understand', + title: 'Read the protocol', + body: 'DIDs, signed ref-updates, UCAN delegation, and ref consensus without a blockchain.', + }, +]; + +export function StartHere() { + return ( +
        + + +
        +

        Start here

        +

        + Everything on this explorer is read live from the node and can be + checked against its API. +

        + +
        + {PATHS.map(path => ( + + {path.step} + + {path.title} + + + {path.body} + + ))} +
        +
        +
        + ); +} diff --git a/src/components/keyboard/CommandPalette.tsx b/src/components/keyboard/CommandPalette.tsx index e0b457f..e92434c 100644 --- a/src/components/keyboard/CommandPalette.tsx +++ b/src/components/keyboard/CommandPalette.tsx @@ -1,11 +1,13 @@ import { useState, useEffect, useMemo, useRef } from 'react'; import { useNavigate, useLocation, matchPath } from 'react-router-dom'; -import { Input } from '@heroui/react'; +import { Input, Kbd } from '../register/controls'; import { useDebouncedValue } from '../../hooks/useDebouncedValue'; import { useShortcuts } from '../../hooks/useShortcuts'; import { fetchRepos, shortDid, SERVER_SEARCH_ENABLED } from '../../lib/api'; import type { ApiRepo } from '../../lib/api'; import { getCachedAgents } from '../../hooks/useAgents'; +import { loadDocSections, searchDocs, docHitPath } from '../../lib/docsSearch'; +import type { DocSection, DocSource } from '../../lib/docsSearch'; import { cn } from '../../lib/utils'; let recentReposCache: ApiRepo[] | null = null; @@ -30,10 +32,24 @@ function loadRecentRepos(): Promise { const TAB_IDS = ['code', 'commits', 'pulls', 'issues', 'certs', 'events']; +/** Mirrors the DOCS list in DocsPage; the search indexes exactly these. */ +const DOC_SOURCES: readonly DocSource[] = [ + { slug: 'quickstart', title: 'Quickstart' }, + { slug: 'agents', title: 'For AI agents' }, + { slug: 'protocol', title: 'Protocol' }, + { slug: 'node', title: 'Run a node' }, +]; + +/** What a row points at, so each kind is recognisable before it is read. */ +type PaletteKind = 'action' | 'repo' | 'agent' | 'doc'; + interface PaletteItem { id: string; label: string; hint?: string; + kind: PaletteKind; + /** Already matched against the query; skip the generic label/hint filter. */ + prematched?: boolean; run: () => void; } @@ -49,6 +65,7 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) { const [query, setQuery] = useState(''); const [sel, setSel] = useState(0); const [repos, setRepos] = useState(SERVER_SEARCH_ENABLED ? [] : recentReposCache); + const [docSections, setDocSections] = useState([]); const listRef = useRef(null); const debouncedQuery = useDebouncedValue(query.trim(), 250); @@ -79,20 +96,31 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) { return () => { cancelled = true; }; }, [open, debouncedQuery]); + // The four docs are ~29KB of markdown, fetched the first time the palette is + // opened rather than at boot: nobody pays for the index who never searches. + useEffect(() => { + if (!open) return; + let cancelled = false; + loadDocSections(DOC_SOURCES) + .then(sections => { if (!cancelled) setDocSections(sections); }) + .catch(() => {}); + return () => { cancelled = true; }; + }, [open]); + const items = useMemo<(PaletteItem & { section: string })[]>(() => { const list: (PaletteItem & { section: string })[] = []; - list.push({ id: 'nav-repos', section: 'actions', label: 'go to repositories', run: () => navigate('/repos') }); - list.push({ id: 'nav-agents', section: 'actions', label: 'go to agents', run: () => navigate('/agents') }); + list.push({ id: 'nav-repos', section: 'actions', kind: 'action', label: 'go to repositories', run: () => navigate('/repos') }); + list.push({ id: 'nav-agents', section: 'actions', kind: 'action', label: 'go to agents', run: () => navigate('/agents') }); if (detailMatch) { list.push({ - id: 'find-file', section: 'actions', label: 'find file', hint: 't', + id: 'find-file', section: 'actions', kind: 'action', label: 'find file', hint: 't', run: () => setOpenModal('finder'), }); for (const tab of TAB_IDS) { list.push({ - id: `tab-${tab}`, section: 'actions', label: `open: ${tab}`, + id: `tab-${tab}`, section: 'actions', kind: 'action', label: `open: ${tab}`, run: () => navigate(`${pathname}${tab === 'code' ? '' : `?tab=${tab}`}`), }); } @@ -106,7 +134,7 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) { ); for (const r of repoRows.slice(0, 12)) { list.push({ - id: `repo-${r.id}`, section: SERVER_SEARCH_ENABLED ? 'repositories' : 'recent repos', + id: `repo-${r.id}`, kind: 'repo', section: SERVER_SEARCH_ENABLED ? 'repositories' : 'recent repos', label: `${shortDid(r.owner_did)}/${r.name}`, hint: r.description ?? undefined, run: () => navigate(`/repos/${r.owner_did}/${r.name}`), @@ -117,20 +145,41 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) { if (agents && query) { for (const a of agents.filter(a => a.did.toLowerCase().includes(query.toLowerCase())).slice(0, 5)) { list.push({ - id: `agent-${a.did}`, section: 'agents', + id: `agent-${a.did}`, kind: 'agent', section: 'agents', label: shortDid(a.did), hint: a.capabilities.join(', '), run: () => navigate(`/repos?owner=${encodeURIComponent(a.did.split(':').pop() ?? '')}`), }); } } + for (const hit of searchDocs(docSections, query)) { + list.push({ + id: `doc-${hit.docSlug}-${hit.id || 'top'}`, + section: 'documentation', + kind: 'doc', + // A document's own top-level heading repeats its title; printing both + // reads as "Quickstart · Quickstart". + label: hit.heading === hit.docTitle ? hit.docTitle : `${hit.docTitle} · ${hit.heading}`, + hint: hit.snippet, + prematched: true, + run: () => navigate(docHitPath(hit)), + }); + } + return list; - }, [query, repos, detailMatch, pathname, navigate, setOpenModal]); + }, [query, repos, docSections, detailMatch, pathname, navigate, setOpenModal]); const filtered = useMemo(() => { if (!query) return items; const q = query.toLowerCase(); - return items.filter(i => i.label.toLowerCase().includes(q) || (i.hint ?? '').toLowerCase().includes(q)); + return items.filter( + i => + // Doc hits matched on section body, which is not shown in full, so the + // label/hint test here would throw most of them away. + i.prematched || + i.label.toLowerCase().includes(q) || + (i.hint ?? '').toLowerCase().includes(q), + ); }, [items, query]); const itemsKey = `${query}:${filtered.length}`; @@ -141,9 +190,11 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) { } useEffect(() => { - if (!listRef.current) return; - const item = listRef.current.children[sel] as HTMLElement | undefined; - item?.scrollIntoView?.({ block: 'nearest' }); + // Section headings are siblings of the rows now, so the selected row is + // found by its id rather than by index into the container's children. + listRef.current + ?.querySelector(`[data-index="${sel}"]`) + ?.scrollIntoView?.({ block: 'nearest' }); }, [sel]); const pick = (item: PaletteItem) => { @@ -152,6 +203,15 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) { }; const handleKeyDown = (e: React.KeyboardEvent) => { + // ShortcutsProvider returns early while a modal is open, so nothing else + // handles Escape here — without this the palette can only be dismissed by + // clicking outside it. + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + onClose(); + return; + } if (e.key === 'ArrowDown') { e.preventDefault(); setSel(s => Math.min(s + 1, filtered.length - 1)); @@ -174,65 +234,163 @@ export function CommandPalette({ open, onClose }: CommandPaletteProps) { if (!open) return null; + const listboxId = 'palette-listbox'; + const activeId = filtered[sel] ? `palette-option-${sel}` : undefined; + return (
        e.stopPropagation()} > - setQuery(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="jump to repo, agent, or action…" - aria-label="command palette" - autoComplete="off" - spellCheck="false" - variant="secondary" - className="w-full h-12 px-5 border-b border-border rounded-none shadow-none" - /> -
        + {/* Search row. The field carries a mark and sits flush in the panel — + a ruled underline inside a framed panel read as a second box. */} +
        +
        + +
        {filtered.map((item, i) => { - const prevSection = i > 0 ? filtered[i - 1].section : ''; - const showSection = item.section !== prevSection; + const showSection = item.section !== (i > 0 ? filtered[i - 1].section : ''); + const selected = i === sel; return ( -
        +
        {showSection && ( -
        +
        {item.section}
        )}
        pick(item)} onPointerEnter={() => setSel(i)} className={cn( - 'flex items-baseline gap-3 px-5 py-2 text-[12.5px] cursor-pointer transition-colors', - i === sel ? 'bg-surface-secondary text-foreground' : 'text-muted', + 'flex cursor-pointer items-center gap-2.5 rounded-[var(--radius-sm)] px-2.5 py-2 transition-colors', + selected ? 'bg-surface-secondary' : 'hover:bg-surface-secondary/60', )} > - {item.label} +
        ); })} + {filtered.length === 0 && ( -
        nothing matches
        +
        +

        No matches for “{query}”

        +

        + Try a repository name, an agent DID, or a phrase from the docs. +

        +
        )}
        -
        - {!SERVER_SEARCH_ENABLED && 'searching the 200 most recently updated repos'} - ↑↓ navigate · ↵ open · esc close + +
        + + {SERVER_SEARCH_ENABLED + ? 'Searching every repository on this node' + : 'Searching the 200 most recently updated repos'} + + + ↑↓ + navigate + + open +
        ); } + +function SearchIcon({ className, ...rest }: React.SVGProps) { + return ( + + + + + ); +} + +/** One mark per kind, so a row's target is legible before the label is read. */ +function KindIcon({ kind, className, ...rest }: { kind: PaletteKind } & React.SVGProps) { + const paths: Record = { + // Arrow — this row performs a navigation. + action: , + // Book — a repository. + repo: , + // Node with links — an agent identity. + agent: <>, + // Page — a document section. + doc: , + }; + return ( + + {paths[kind]} + + ); +} diff --git a/src/components/keyboard/KeyboardCheatsheet.tsx b/src/components/keyboard/KeyboardCheatsheet.tsx index 7df6a6f..3738295 100644 --- a/src/components/keyboard/KeyboardCheatsheet.tsx +++ b/src/components/keyboard/KeyboardCheatsheet.tsx @@ -1,6 +1,7 @@ -import { Kbd as HeroKbd, Button } from '@heroui/react'; +import { Kbd as HeroKbd, Button } from '../register/controls'; import { Modal } from '../ui/Modal'; import { MicroLabel } from '../ui/MicroLabel'; +import { modifierKeyLabel } from '../../lib/platform'; function Kbd({ children }: { children: string }) { return {children}; @@ -39,7 +40,7 @@ export function KeyboardCheatsheet({ open, onClose }: KeyboardCheatsheetProps) {
        global - + diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index e5b2c92..4243144 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -2,10 +2,12 @@ import { Outlet } from 'react-router-dom'; import TopBar from './TopBar'; import Footer from './Footer'; import { ShortcutsProvider } from '../keyboard/ShortcutsProvider'; +import { ScrollToTop } from './ScrollToTop'; export default function AppLayout() { return ( +
        diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 4a02127..3cdafbe 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,28 +1,79 @@ +import { Link } from 'react-router-dom'; import { CopyButton } from '../ui/CopyButton'; import { truncateDid } from '../../lib/api'; import { useNodeStatus } from '../../hooks/useNodeStatus'; +const linkCls = 'text-[13px] text-muted hover:text-accent transition-colors'; + export default function Footer() { const { node, stats } = useNodeStatus(); const version = node?.version ?? stats?.version; return (
        -
        - - gitlawb explorer - {version &&  v{version}} - {node?.network &&  {node.network}} - +
        +
        + +
        +

        gitlawb explorer

        +

        + {version && <>v{version}} + {node?.network && ( + <> + · + {node.network} + + )} +

        + {node?.did && ( + + + {truncateDid(node.did)} + + + + )} +
        + + +
        - {node?.did && ( - - - {truncateDid(node.did)} - - - - )} +

        + The explorer holds no data of its own — every figure here is read live from the node and + can be checked against its API. +

        ); diff --git a/src/components/layout/ScrollToTop.tsx b/src/components/layout/ScrollToTop.tsx new file mode 100644 index 0000000..86d3d26 --- /dev/null +++ b/src/components/layout/ScrollToTop.tsx @@ -0,0 +1,42 @@ +import { useLayoutEffect, useRef } from 'react'; +import { useLocation, useNavigationType } from 'react-router-dom'; + +/** + * Reset the scroll position when the visitor moves to a different page. + * + * A browser scrolls to the top on a real navigation; a single-page app does + * not, because nothing about the document changes. Without this, following the + * "Next" pager at the foot of a document loaded the next document and left the + * reader parked at the bottom of it, looking at the pager again — the content + * had changed but the viewport had not moved. + * + * Three cases are deliberately left alone: + * + * - Back and forward. `POP` means the browser is restoring a position the + * visitor already had, and overriding that loses their place. + * - Anything carrying a hash, which names a specific heading to land on. The + * page owning that anchor scrolls to it once its content has rendered. + * - Query-string changes. Search, filters, sorting and paging all rewrite the + * query while staying on the same page; scrolling on those would yank the + * view to the top on every keystroke typed into a search field. + */ +export function ScrollToTop() { + const { pathname, hash } = useLocation(); + const navigationType = useNavigationType(); + const previousPathname = useRef(pathname); + + useLayoutEffect(() => { + const changedPage = previousPathname.current !== pathname; + previousPathname.current = pathname; + + if (!changedPage) return; + if (navigationType === 'POP') return; + if (hash) return; + + // Instant, not smooth: this is a page change, and animating it makes the + // new page appear to scroll past content the visitor never asked to see. + window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); + }, [pathname, hash, navigationType]); + + return null; +} diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index b0f2043..b390fae 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -1,21 +1,13 @@ +import { useEffect, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { - Chip, - Drawer, - DrawerTrigger, - DrawerBackdrop, - DrawerContent, - DrawerDialog, - DrawerHeader, - DrawerBody, - DrawerCloseTrigger, - useOverlayState, -} from '@heroui/react'; +import { AnimatePresence, motion, useReducedMotion } from 'motion/react'; +import { Menu, X, Search } from 'lucide-react'; import { ThemeToggle } from '../ui/ThemeToggle'; -import { ThemeCustomizer } from '../ui/ThemeCustomizer'; import { Logo } from '../ui/Logo'; import { useNodeStatus } from '../../hooks/useNodeStatus'; +import { useShortcuts } from '../../hooks/useShortcuts'; import { cn } from '../../lib/utils'; +import { modifierKeyLabel } from '../../lib/platform'; const NAV_LINKS = [ { to: '/', label: 'overview', end: true }, @@ -31,118 +23,164 @@ const NAV_LINKS = [ export default function TopBar() { const { node, stats } = useNodeStatus(); const location = useLocation(); - const drawer = useOverlayState(); + const { setOpenModal } = useShortcuts(); + const [menuOpen, setMenuOpen] = useState(false); + const [hovered, setHovered] = useState(null); + const reduce = useReducedMotion(); + const paletteHint = `${modifierKeyLabel()} K`; + + // Close on route change during render — an effect here trips + // react-hooks/set-state-in-effect and lands a frame late. + const [prevPath, setPrevPath] = useState(location.pathname); + if (prevPath !== location.pathname) { + setPrevPath(location.pathname); + if (menuOpen) setMenuOpen(false); + } + + useEffect(() => { + if (!menuOpen) return; + const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setMenuOpen(false); + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, [menuOpen]); const isActive = (to: string, end?: boolean) => end ? location.pathname === to : location.pathname.startsWith(to + '/') || location.pathname === to; + const activeHref = NAV_LINKS.find(l => isActive(l.to, l.end))?.to; + // The rail follows the pointer while hovering and returns to the current + // route on leave, so it always reports where a click would take you. + const railOn = hovered ?? activeHref; + return ( -
        -
        - - - explorer +
        +
        + + + + gitlawb + -
        ); } diff --git a/src/components/network/FederationMap.tsx b/src/components/network/FederationMap.tsx new file mode 100644 index 0000000..d29a099 --- /dev/null +++ b/src/components/network/FederationMap.tsx @@ -0,0 +1,104 @@ +import { useRef } from 'react'; +import { Server, Radio } from 'lucide-react'; +import type { NodeSnapshot } from '../../lib/nodes'; +import { AnimatedBeam } from '../ui/animated-beam'; +import { cn } from '../../lib/utils'; + +/** + * The federation, drawn as the gossip graph it is. + * + * The first node in the list is the one this explorer is pointed at; the rest + * are its federated peers. A beam runs from the origin to each peer that is + * actually reachable — an unreachable node gets a dead line, because animating + * gossip into a node that is not answering would be a decorative lie about the + * one thing this page exists to report. + */ +/** Hoisted: defining this inside FederationMap would remount every node on + * each render and trips react-hooks' create-components-during-render rule. */ +function NodeChip({ + snap, + innerRef, + lead = false, +}: { + snap: NodeSnapshot; + innerRef: React.RefObject; + lead?: boolean; +}) { + return ( +
        + + {lead ? : } + + + {snap.node.label} + + {snap.stats && ( + + {snap.stats.repos.toLocaleString()} + + )} +
        + ); +} + +export function FederationMap({ snapshots }: { snapshots: NodeSnapshot[] }) { + const containerRef = useRef(null); + const originRef = useRef(null); + const peerRef0 = useRef(null); + const peerRef1 = useRef(null); + const peerRef2 = useRef(null); + const peerRef3 = useRef(null); + const peerRefs = [peerRef0, peerRef1, peerRef2, peerRef3]; + + if (snapshots.length < 2) return null; + const [origin, ...peers] = snapshots; + const shown = peers.slice(0, 4); + + return ( +
        + + +
        + {shown.map((s, i) => ( + + ))} +
        + + {shown.map((s, i) => ( + + ))} +
        + ); +} diff --git a/src/components/network/NetworkGlobe.tsx b/src/components/network/NetworkGlobe.tsx new file mode 100644 index 0000000..10c1903 --- /dev/null +++ b/src/components/network/NetworkGlobe.tsx @@ -0,0 +1,66 @@ +import { useMemo } from 'react'; +import type { COBEOptions } from 'cobe'; +import { Globe } from '../ui/globe'; +import { useIsDark } from '../../hooks/useIsDark'; + +/** + * A globe behind the network header. + * + * Deliberately illustrative. The node API exposes no geography — a peer record + * is `{ did, http_url, last_seen, reachable }` and nothing more — so these + * markers are a stock world spread, not peer locations. The section is framed + * that way on purpose: a map pin gets read as a claim about where a machine + * is, and the real federation view lives in the topology diagram and the node + * cards below, which are driven by actual reachability. + */ + +/** A stock spread of world cities. Decorative — not peer positions. */ +const ILLUSTRATIVE_MARKERS: COBEOptions['markers'] = [ + { location: [14.5995, 120.9842], size: 0.03 }, + { location: [19.076, 72.8777], size: 0.06 }, + { location: [30.0444, 31.2357], size: 0.05 }, + { location: [39.9042, 116.4074], size: 0.06 }, + { location: [-23.5505, -46.6333], size: 0.06 }, + { location: [19.4326, -99.1332], size: 0.05 }, + { location: [40.7128, -74.006], size: 0.07 }, + { location: [51.5072, -0.1276], size: 0.06 }, + { location: [35.6762, 139.6503], size: 0.05 }, + { location: [-33.8688, 151.2093], size: 0.04 }, +]; + +export function NetworkGlobe() { + const isDark = useIsDark(); + + // COBEOptions is a complete object; the component replaces its default + // wholesale rather than merging, so every field has to be supplied. The + // sphere is painted into a canvas, so its colours cannot come from CSS + // tokens and have to be switched here when the theme flips — a dark sphere + // on the light ground reads as a hole in the page. + const config = useMemo( + () => ({ + width: 800, + height: 800, + onRender: () => {}, + devicePixelRatio: 2, + phi: 0, + theta: 0.3, + dark: isDark ? 1 : 0, + diffuse: isDark ? 0.4 : 1.2, + mapSamples: 16000, + mapBrightness: isDark ? 1.2 : 6, + // Monochrome sphere either way; markers keep the one colour this palette + // carries, darkened on light so they stay legible. + baseColor: isDark ? [0.32, 0.32, 0.32] : [0.86, 0.86, 0.86], + glowColor: isDark ? [0.12, 0.12, 0.12] : [0.9, 0.9, 0.9], + markerColor: isDark ? [0.02, 0.87, 0.45] : [0, 0.44, 0.18], + markers: ILLUSTRATIVE_MARKERS, + }), + [isDark], + ); + + return ( +
        + +
        + ); +} diff --git a/src/components/network/NodeCard.tsx b/src/components/network/NodeCard.tsx index 663e7c2..175c312 100644 --- a/src/components/network/NodeCard.tsx +++ b/src/components/network/NodeCard.tsx @@ -1,31 +1,29 @@ -import { Circle } from 'lucide-react'; +import { Circle, ExternalLink } from 'lucide-react'; import { cn } from '../../lib/utils'; import type { NodeSnapshot } from '../../lib/nodes'; import { truncateDid } from '../../lib/api'; -import { MicroLabel } from '../ui/MicroLabel'; import { CopyButton } from '../ui/CopyButton'; import { Skeleton } from '../ui/Skeleton'; +import { MagicCard } from '../ui/magic-card'; +import { NumberTicker } from '../ui/number-ticker'; -function Metric({ label, value }: { label: string; value: string }) { +function Metric({ label, value }: { label: string; value: number | null }) { return ( -
        -

        {value}

        - {label} +
        +

        + {value === null ? '—' : } +

        + {label}
        ); } export function NodeCardSkeleton() { return ( -
        -
        - -
        -
        - - - -
        +
        + + +
        ); } @@ -34,56 +32,68 @@ export function NodeCard({ snapshot }: { snapshot: NodeSnapshot }) { const { node, reachable, info, stats, peers, p2p } = snapshot; return ( -
        -
        - - - - {node.label} ↗ - - - {info?.version && v{info.version}} -
        - -
        - {info?.did ? ( + +
        +
        - - {truncateDid(info.did)} - - + - ) : ( - {reachable ? 'identity unavailable' : 'node unreachable'} - )} + {info?.version && ( + v{info.version} + )} +
        - {p2p?.enabled && ( - - libp2p · {p2p.connected_peers ?? 0} connected · {peers?.length ?? 0} known peers - - )} -
        +
        + {info?.did ? ( + + + {truncateDid(info.did)} + + + + ) : ( + + {reachable ? 'Identity unavailable' : 'Node unreachable'} + + )} -
        - -
        - + {p2p?.enabled && ( + + libp2p · {p2p.connected_peers ?? 0} connected · {peers?.length ?? 0} known + + )}
        -
        - + +
        + + +
        -
        + ); } diff --git a/src/components/network/ReplicationTable.tsx b/src/components/network/ReplicationTable.tsx index 58fcd58..96b844f 100644 --- a/src/components/network/ReplicationTable.tsx +++ b/src/components/network/ReplicationTable.tsx @@ -21,8 +21,8 @@ export function ReplicationTable({ replication, labels, rowLimit = 12 }: Replica const short = (label: string) => label.split('.')[0]; return ( -
        -
        +
        +
        replication — latest ref updates across nodes {replication.fullyReplicatedCount}/{replication.totalTuples} in sync ·{' '} @@ -36,7 +36,7 @@ export function ReplicationTable({ replication, labels, rowLimit = 12 }: Replica
        {/* Header */}
        repo · ref diff --git a/src/components/peers/PeerList.tsx b/src/components/peers/PeerList.tsx index 011e5d5..7252012 100644 --- a/src/components/peers/PeerList.tsx +++ b/src/components/peers/PeerList.tsx @@ -1,6 +1,6 @@ import { Circle } from 'lucide-react'; import { useState } from 'react'; -import { Button, Link } from '@heroui/react'; +import { Button } from '../register/controls'; import { cn } from '../../lib/utils'; import type { ApiPeer } from '../../lib/api'; import { pingPeer, peerHost, shortDid, timeAgo } from '../../lib/api'; @@ -74,25 +74,25 @@ function PeerRow({ peer, index }: { peer: ApiPeer; index: number }) { {shortDid(peer.did)} - {peerHost(peer.http_url)} - +
        {/* Host */} - {peerHost(peer.http_url)} ↗ - + {/* Last seen */} @@ -129,10 +129,10 @@ interface PeerListProps { export function PeerList({ peers, loading = false, skeletonCount = 10 }: PeerListProps) { return (
        -
        +
        peer endpoint diff --git a/src/components/peers/PeerOrbit.tsx b/src/components/peers/PeerOrbit.tsx new file mode 100644 index 0000000..9423e3b --- /dev/null +++ b/src/components/peers/PeerOrbit.tsx @@ -0,0 +1,55 @@ +import { Server } from 'lucide-react'; +import type { ApiPeer } from '../../lib/api'; +import { shortDid, peerHost } from '../../lib/api'; +import { OrbitingCircles } from '../ui/orbiting-circles'; +import { cn } from '../../lib/utils'; + +/** + * The federation, drawn as what it is: this node at the centre with its + * reachable peers circling it. + * + * Magic UI's OrbitingCircles carries the motion. Only reachable peers orbit — + * an unreachable node is not participating, and showing it circling would be a + * decorative lie. Two rings so a node with many peers stays legible. + */ +export function PeerOrbit({ peers, nodeName }: { peers: ApiPeer[]; nodeName?: string }) { + const reachable = peers.filter(p => p.reachable); + if (reachable.length === 0) return null; + + const inner = reachable.slice(0, 5); + const outer = reachable.slice(5, 12); + + const Chip = ({ peer }: { peer: ApiPeer }) => ( + + {peerHost(peer.http_url).split('.')[0].slice(0, 10) || shortDid(peer.did).slice(0, 6)} + + ); + + return ( +
        +
        + + + + {nodeName ?? 'this node'} + {reachable.length} peers up +
        + + + {inner.map(p => )} + + + {outer.length > 0 && ( + + {outer.map(p => )} + + )} +
        + ); +} diff --git a/src/components/register/SearchField.tsx b/src/components/register/SearchField.tsx new file mode 100644 index 0000000..c1324a7 --- /dev/null +++ b/src/components/register/SearchField.tsx @@ -0,0 +1,49 @@ +import { Search } from 'lucide-react'; +import { Input } from './controls'; +import { cn } from '../../lib/utils'; + +interface SearchFieldProps { + id: string; + value: string; + onChange: (v: string) => void; + placeholder: string; + /** Accessible name; the field carries its icon instead of a visible label. */ + label: string; + className?: string; +} + +/** + * The search field used on every listing page. + * + * One shape, defined once. Each page previously rolled its own — a stacked + * caption above a square input — which meant four slightly different search + * bars and four places to fix anything. + */ +export function SearchField({ + id, + value, + onChange, + placeholder, + label, + className, +}: SearchFieldProps) { + return ( +
        +
        + ); +} diff --git a/src/components/register/Tabs.tsx b/src/components/register/Tabs.tsx new file mode 100644 index 0000000..7e8292d --- /dev/null +++ b/src/components/register/Tabs.tsx @@ -0,0 +1,153 @@ +import { + createContext, + useContext, + useId, + useRef, + type ReactNode, + type KeyboardEvent, +} from 'react'; +import { cn } from '../../lib/utils'; + +/** + * Tabs, underline-indicator style. + * + * Arrow-key roving and correct tab/tabpanel wiring are kept from the kit this + * replaces. + */ + +interface TabsCtx { + selected: string; + select: (id: string) => void; + baseId: string; +} + +const Ctx = createContext(null); + +function useTabs(): TabsCtx { + const ctx = useContext(Ctx); + if (!ctx) throw new Error('Tabs subcomponent used outside '); + return ctx; +} + +interface TabsProps { + selectedKey: string; + onSelectionChange: (key: string) => void; + children: ReactNode; +} + +export function Tabs({ selectedKey, onSelectionChange, children }: TabsProps) { + const baseId = useId(); + return ( + + {children} + + ); +} + +function ListContainer({ children, className }: { children: ReactNode; className?: string }) { + return
        {children}
        ; +} + +function List({ children, ...rest }: { children: ReactNode; 'aria-label'?: string }) { + const listRef = useRef(null); + + // Roving arrow keys across the tab strip, wrapping at both ends. + const onKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; + const tabs = Array.from( + listRef.current?.querySelectorAll('[role="tab"]') ?? [], + ); + const i = tabs.findIndex(t => t === document.activeElement); + if (i === -1) return; + e.preventDefault(); + const next = e.key === 'ArrowRight' ? (i + 1) % tabs.length : (i - 1 + tabs.length) % tabs.length; + tabs[next].focus(); + tabs[next].click(); + }; + + return ( +
        + {children} +
        + ); +} + +function Tab({ + id, + children, + className, +}: { + id: string; + children: ReactNode; + className?: string; +}) { + const { selected, select, baseId } = useTabs(); + const active = selected === id; + return ( + + ); +} + +/** Kept for call-site compatibility; the indicator is drawn by Tab itself. */ +function Indicator() { + return null; +} + +function Panel({ + id, + children, + className, +}: { + id: string; + children: ReactNode; + className?: string; +}) { + const { selected, baseId } = useTabs(); + if (selected !== id) return null; + return ( +
        + {children} +
        + ); +} + +Tabs.ListContainer = ListContainer; +Tabs.List = List; +Tabs.Tab = Tab; +Tabs.Indicator = Indicator; +Tabs.Panel = Panel; diff --git a/src/components/register/controls.tsx b/src/components/register/controls.tsx new file mode 100644 index 0000000..435055b --- /dev/null +++ b/src/components/register/controls.tsx @@ -0,0 +1,215 @@ +import type { + ButtonHTMLAttributes, + InputHTMLAttributes, + ReactNode, +} from 'react'; +import { cn } from '../../lib/utils'; + +/** + * Controls. + * + * Rules and list hairlines are drawn square; controls are not. A control at + * zero radius reads as a hard box, and a listing page puts many of them side by + * side, so they take `--radius-control` — a full round, matching the search + * field. Buttons stay frameless by default, and a border or a fill is reserved + * for the rare control that must stand apart. + * + * `onPress` / `isDisabled` are accepted alongside `onClick` / `disabled` so + * call sites migrating off the old kit keep working. + */ + +const focus = + 'focus:outline-none focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-[-1px]'; + +/* ── Input ────────────────────────────────────────────────────────────── */ +export interface InputProps + extends Omit, 'size'> { + variant?: string; +} + +export function Input({ className, variant: _variant, ...rest }: InputProps) { + void _variant; + return ( + + ); +} + +/* ── Button ───────────────────────────────────────────────────────────── */ +export interface ButtonProps + extends Omit, 'onClick'> { + children?: ReactNode; + onClick?: ButtonHTMLAttributes['onClick']; + onPress?: () => void | Promise; + isDisabled?: boolean; + variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | string; + size?: 'sm' | 'md' | string; + fullWidth?: boolean; + isIconOnly?: boolean; +} + +export function Button({ + children, + className, + variant = 'secondary', + size = 'md', + fullWidth, + isIconOnly, + onClick, + onPress, + isDisabled, + disabled, + type = 'button', + ...rest +}: ButtonProps) { + const off = isDisabled ?? disabled; + + // A bordered control at list density is hundreds of frames on one page, which + // is what made the surface feel boxy. The default is frameless: quiet text + // that lifts on hover. A frame is reserved for `outline`, and a fill for the + // one primary action on a view. + const tones: Record = { + primary: 'bg-foreground text-background font-medium hover:opacity-90', + secondary: 'text-muted hover:text-foreground hover:bg-surface-secondary', + tertiary: 'text-muted hover:text-foreground', + outline: 'text-foreground border border-border hover:border-foreground/40', + danger: 'text-danger hover:bg-danger hover:text-background', + }; + + return ( + + ); +} + +/* ── Select ─────────────────────────────────────────────────────────────── + Removed. The native control paints its option list as an operating-system + popup that cannot take this page's colours or its zero-radius identity, and + styling the closed control did nothing to the opened one. Use `Dropdown` + (components/ui/Dropdown.tsx), which renders the list in the page. */ + +/* ── Kbd ──────────────────────────────────────────────────────────────── */ +export function Kbd({ children, className }: { children: ReactNode; className?: string }) { + return ( + + {children} + + ); +} + +Kbd.Content = function KbdContent({ children }: { children: ReactNode }) { + return <>{children}; +}; + +/* ── Skeleton ─────────────────────────────────────────────────────────── */ +export function Skeleton({ className }: { className?: string }) { + return ( +
        )}
        -
        + ); -} \ No newline at end of file +} diff --git a/src/components/repos/RepoList.tsx b/src/components/repos/RepoList.tsx index 09ded60..0893996 100644 --- a/src/components/repos/RepoList.tsx +++ b/src/components/repos/RepoList.tsx @@ -1,6 +1,5 @@ import { cn } from '../../lib/utils'; import type { Repository } from '../../types/repo'; -import { MicroLabel } from '../ui/MicroLabel'; import { RepoRow } from './RepoRow'; import { RepoRowSkeleton } from './RepoRowSkeleton'; @@ -12,31 +11,36 @@ interface RepoListProps { emptyMessage?: string; } +/** + * The repository list. + * + * One bordered container with hairline-separated rows — the shape a file + * listing takes on a forge. There is no column-header strip: the rows are not + * a uniform table (description wraps, badges come and go), and a header grid + * silently drifts out of alignment with them, which is exactly what happened + * to the previous four-column version. + */ export function RepoList({ repos, loading = false, skeletonCount = 10, - emptyMessage = 'no repositories match', + emptyMessage = 'No repositories match', }: RepoListProps) { return ( -
        - {/* Header strip */} -
        - - repository - activity - updated -
        - +
        {repos === null ? (
          {Array.from({ length: skeletonCount }, (_, i) => )}
        ) : repos.length === 0 ? ( -

        {emptyMessage}

        +

        {emptyMessage}

        ) : (
          {repos.map((repo, i) => )} diff --git a/src/components/repos/RepoPagination.tsx b/src/components/repos/RepoPagination.tsx index 8eaf2b7..4653db8 100644 --- a/src/components/repos/RepoPagination.tsx +++ b/src/components/repos/RepoPagination.tsx @@ -1,20 +1,6 @@ -import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink, - PaginationPrevious, - PaginationNext, - PaginationEllipsis, - PaginationSummary, - Select, - SelectTrigger, - SelectValue, - SelectPopover, - Menu, - MenuItem, -} from '@heroui/react'; +import { Dropdown } from '../ui/Dropdown'; import { PER_PAGE_OPTIONS } from '../../lib/constants'; +import { cn } from '../../lib/utils'; interface RepoPaginationProps { page: number; @@ -32,6 +18,7 @@ function range(start: number, end: number): number[] { return Array.from({ length: end - start + 1 }, (_, i) => start + i); } +/** Page controls for a server-paginated list. */ export function RepoPagination({ page, totalPages, @@ -51,72 +38,76 @@ export function RepoPagination({ ? [1, 0, ...range(totalPages - 4, totalPages)] : [1, 0, page - 1, page, page + 1, 0, totalPages]; + const step = + 'text-[13px] px-2 py-1 rounded-[var(--radius)] transition-colors ' + + 'disabled:opacity-30 disabled:cursor-not-allowed'; + return ( -