frierena personal git archive

frieren

a self-hosted git server in one binary — everyone reads, only the owner writes

feat(web): Next.js frontend for the archive

9bc8cf25e7e31fc939c7d6a42e28c5f717c9a47d

justin06lee · Aug 18, 2026, 11:24 PM (4h ago)

Makefile                                       |   5 +-
 README.md                                      |  17 +
 web/.gitignore                                 |  41 ++
 web/app/[repo]/blob/[ref]/[...path]/page.tsx   |  66 ++++
 web/app/[repo]/commit/[hash]/page.tsx          |  33 ++
 web/app/[repo]/commits/[[...ref]]/page.tsx     |  46 +++
 web/app/[repo]/layout.tsx                      |  40 ++
 web/app/[repo]/page.tsx                        |  47 +++
 web/app/[repo]/refs/page.tsx                   |  62 +++
 web/app/[repo]/tree/[ref]/[[...path]]/page.tsx |  23 ++
 web/app/error.tsx                              |  20 +
 web/app/globals.css                            | 130 +++++++
 web/app/icon.svg                               |  12 +
 web/app/layout.tsx                             |  46 +++
 web/app/not-found.tsx                          |  15 +
 web/app/page.tsx                               |  63 ++++
 web/bun.lock                                   | 497 +++++++++++++++++++++++++
 web/components/CloneBox.tsx                    |  26 ++
 web/components/Crumbs.tsx                      |  41 ++
 web/components/Diff.tsx                        |  56 +++
 web/components/Markdown.tsx                    |  43 +++
 web/components/Offline.tsx                     |  31 ++
 web/components/RepoNav.tsx                     |  46 +++
 web/components/Starfield.tsx                   |  34 ++
 web/components/TreeTable.tsx                   |  63 ++++
 web/lib/api.ts                                 |  96 +++++
 web/lib/diff.ts                                |  71 ++++
 web/lib/format.ts                              |  31 ++
 web/lib/params.ts                              |  13 +
 web/lib/shiki.ts                               |  46 +++
 web/next.config.ts                             |   7 +
 web/package.json                               |  37 ++
 web/postcss.config.mjs                         |   7 +
 web/tsconfig.json                              |  34 ++
 34 files changed, 1844 insertions(+), 1 deletion(-)
Makefile+41
@@ -1,7 +1,7 @@
11VERSION := $(shell git describe --tags --match 'v*' --always --dirty 2>/dev/null || echo dev)
22INSTALL_DIR := $(HOME)/.local/bin
33
4.PHONY: all build install update test clean
4+.PHONY: all build install update test web clean
55
66all: build install
77
@@ -21,5 +21,8 @@ update: all
2121test:
2222 go test ./...
2323
24+web:
25+ cd web && bun install && bun run build
26+
2427clean:
2528 rm -rf dist
README.md+170
@@ -92,6 +92,23 @@ GET /api/repos/{name}/refs branches and tags
9292
9393Omitting `ref` uses the repository's default branch.
9494
95+## The frontend (`web/`)
96+
97+`web/` is a Next.js app that reads that API and turns the archive into a designed reading experience — serif hero, syntax-highlighted files (shiki), rendered READMEs with working relative images, per-file diff views with dual line numbers. It fetches server-side, so the backend needs no CORS and its address stays out of the browser.
98+
99+```sh
100+cd web
101+bun install
102+FRIEREN_API_URL=http://localhost:7420 bun dev
103+```
104+
105+It deploys anywhere Next.js runs; on Vercel, set two things on the project:
106+
107+- `FRIEREN_API_URL` — the public URL of your frieren backend (e.g. `https://git.example.com`)
108+- `FRIEREN_CLONE_URL` — optional; shown in clone commands when it differs from the API URL
109+
110+Until the backend is reachable, the site renders a graceful "archive unreachable" state with setup instructions, and recovers on its own once the server answers.
111+
95112## What it deliberately isn't
96113
97114No issues, no pull requests, no user accounts, no markdown rendering yet — it hosts and shows git repositories, and stops there. The single-writer model is the point: if you need collaborators with write access, you want a full forge like Forgejo.
web/.gitignore+410
@@ -0,0 +1,41 @@
1+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+# dependencies
4+/node_modules
5+/.pnp
6+.pnp.*
7+.yarn/*
8+!.yarn/patches
9+!.yarn/plugins
10+!.yarn/releases
11+!.yarn/versions
12+
13+# testing
14+/coverage
15+
16+# next.js
17+/.next/
18+/out/
19+
20+# production
21+/build
22+
23+# misc
24+.DS_Store
25+*.pem
26+
27+# debug
28+npm-debug.log*
29+yarn-debug.log*
30+yarn-error.log*
31+.pnpm-debug.log*
32+
33+# env files (can opt-in for committing if needed)
34+.env*
35+
36+# vercel
37+.vercel
38+
39+# typescript
40+*.tsbuildinfo
41+next-env.d.ts
web/app/[repo]/blob/[ref]/[...path]/page.tsx+660
@@ -0,0 +1,66 @@
1+import { notFound } from "next/navigation";
2+import { getBlob, rawUrl } from "@/lib/api";
3+import { dec, decPath } from "@/lib/params";
4+import { byteSize } from "@/lib/format";
5+import { highlight, langForFile } from "@/lib/shiki";
6+import Crumbs from "@/components/Crumbs";
7+
8+type Props = { params: Promise<{ repo: string; ref: string; path: string[] }> };
9+
10+export default async function BlobPage({ params }: Props) {
11+ const p = await params;
12+ const repo = dec(p.repo);
13+ const ref = dec(p.ref);
14+ const path = decPath(p.path);
15+ const blob = await getBlob(repo, ref, path);
16+ if (!blob) notFound();
17+
18+ const html = blob.binary || blob.truncated ? null : await highlight(blob.content, langForFile(path));
19+
20+ return (
21+ <div className="space-y-4">
22+ <div className="flex flex-wrap items-baseline justify-between gap-3">
23+ <Crumbs repo={repo} refName={ref} path={path} leafIsLink={false} />
24+ <p className="font-mono text-xs text-dim">
25+ {byteSize(blob.size)} ·{" "}
26+ <a href={rawUrl(repo, ref, path)} className="text-fog hover:text-frost">
27+ raw
28+ </a>
29+ </p>
30+ </div>
31+ {blob.binary ? (
32+ <p className="border border-line bg-panel px-4 py-8 text-sm text-fog">
33+ Binary file.{" "}
34+ <a href={rawUrl(repo, ref, path)} className="text-frost hover:underline">
35+ Download the raw bytes.
36+ </a>
37+ </p>
38+ ) : blob.truncated ? (
39+ <p className="border border-line bg-panel px-4 py-8 text-sm text-fog">
40+ Too large to display here.{" "}
41+ <a href={rawUrl(repo, ref, path)} className="text-frost hover:underline">
42+ View raw instead.
43+ </a>
44+ </p>
45+ ) : html ? (
46+ <div
47+ className="overflow-x-auto border border-line bg-abyss px-0 py-3 [&_pre]:px-4"
48+ dangerouslySetInnerHTML={{ __html: html }}
49+ />
50+ ) : (
51+ <div className="overflow-x-auto border border-line bg-abyss px-4 py-3">
52+ <pre className="shiki">
53+ <code>
54+ {blob.content.replace(/\n$/, "").split("\n").map((line, i) => (
55+ <span key={i} className="line">
56+ {line}
57+ {"\n"}
58+ </span>
59+ ))}
60+ </code>
61+ </pre>
62+ </div>
63+ )}
64+ </div>
65+ );
66+}
web/app/[repo]/commit/[hash]/page.tsx+330
@@ -0,0 +1,33 @@
1+import { notFound } from "next/navigation";
2+import { getCommit } from "@/lib/api";
3+import { dec } from "@/lib/params";
4+import { fullDate, timeAgo } from "@/lib/format";
5+import Diff from "@/components/Diff";
6+
7+type Props = { params: Promise<{ repo: string; hash: string }> };
8+
9+export default async function CommitPage({ params }: Props) {
10+ const p = await params;
11+ const repo = dec(p.repo);
12+ const commit = await getCommit(repo, dec(p.hash));
13+ if (!commit) notFound();
14+
15+ return (
16+ <div className="space-y-6">
17+ <header className="border border-line bg-panel px-5 py-4">
18+ <h2 className="text-lg text-snow">{commit.subject}</h2>
19+ <p className="mt-2 font-mono text-xs text-dim">{commit.hash}</p>
20+ <p className="mt-1 font-mono text-xs text-fog">
21+ {commit.author} · {fullDate(commit.when)} ({timeAgo(commit.when)})
22+ </p>
23+ </header>
24+ <Diff patch={commit.patch} />
25+ {commit.truncated && (
26+ <p className="font-mono text-xs text-dim">
27+ diff truncated — see the full change locally with{" "}
28+ <code className="text-fog">git show {commit.short}</code>
29+ </p>
30+ )}
31+ </div>
32+ );
33+}
web/app/[repo]/commits/[[...ref]]/page.tsx+460
@@ -0,0 +1,46 @@
1+import Link from "next/link";
2+import { notFound } from "next/navigation";
3+import { getRepo, getCommits } from "@/lib/api";
4+import { dec, decPath } from "@/lib/params";
5+import { timeAgo } from "@/lib/format";
6+
7+type Props = { params: Promise<{ repo: string; ref?: string[] }> };
8+
9+export default async function CommitsPage({ params }: Props) {
10+ const p = await params;
11+ const repo = dec(p.repo);
12+ const info = await getRepo(repo);
13+ if (!info) notFound();
14+ const ref = p.ref?.length ? decPath(p.ref) : info.default;
15+ const commits = await getCommits(repo, ref);
16+ if (!commits) notFound();
17+
18+ return (
19+ <div className="space-y-4">
20+ <p className="font-mono text-xs text-dim">
21+ history of <span className="text-frost">{ref}</span>
22+ {commits.length === 100 && " · latest 100"}
23+ </p>
24+ {commits.length === 0 ? (
25+ <p className="border border-line bg-panel px-4 py-8 text-sm text-fog">No commits yet.</p>
26+ ) : (
27+ <div className="border border-line bg-panel">
28+ {commits.map((c) => (
29+ <Link
30+ key={c.hash}
31+ href={`/${repo}/commit/${c.hash}`}
32+ className="flex items-baseline gap-4 border-b border-line px-4 py-2.5 text-sm last:border-b-0 hover:bg-raise"
33+ >
34+ <span className="shrink-0 font-mono text-xs text-gold/80">{c.short}</span>
35+ <span className="min-w-0 flex-1 truncate text-snow">{c.subject}</span>
36+ <span className="hidden shrink-0 font-mono text-xs text-dim sm:inline">
37+ {c.author}
38+ </span>
39+ <span className="shrink-0 font-mono text-xs text-dim">{timeAgo(c.when)}</span>
40+ </Link>
41+ ))}
42+ </div>
43+ )}
44+ </div>
45+ );
46+}
web/app/[repo]/layout.tsx+400
@@ -0,0 +1,40 @@
1+import { notFound } from "next/navigation";
2+import type { Metadata } from "next";
3+import { getRepo, cloneUrl, BackendOffline } from "@/lib/api";
4+import { dec } from "@/lib/params";
5+import RepoNav from "@/components/RepoNav";
6+import CloneBox from "@/components/CloneBox";
7+import Offline from "@/components/Offline";
8+
9+type Props = { children: React.ReactNode; params: Promise<{ repo: string }> };
10+
11+export async function generateMetadata({ params }: { params: Promise<{ repo: string }> }): Promise<Metadata> {
12+ const { repo } = await params;
13+ return { title: dec(repo) };
14+}
15+
16+export default async function RepoLayout({ children, params }: Props) {
17+ const name = dec((await params).repo);
18+ let repo;
19+ try {
20+ repo = await getRepo(name);
21+ } catch (e) {
22+ if (e instanceof BackendOffline) return <Offline />;
23+ throw e;
24+ }
25+ if (!repo) notFound();
26+
27+ return (
28+ <div>
29+ <div className="mb-6 flex flex-wrap items-start justify-between gap-4">
30+ <div>
31+ <h1 className="font-display text-3xl">{repo.name}</h1>
32+ {repo.description && <p className="mt-1 text-sm text-fog">{repo.description}</p>}
33+ </div>
34+ <CloneBox url={cloneUrl(repo.name)} />
35+ </div>
36+ <RepoNav repo={repo.name} defaultBranch={repo.default} />
37+ <div className="pt-6">{children}</div>
38+ </div>
39+ );
40+}
web/app/[repo]/page.tsx+470
@@ -0,0 +1,47 @@
1+import { notFound } from "next/navigation";
2+import { getRepo, getTree, getReadme, cloneUrl } from "@/lib/api";
3+import { dec } from "@/lib/params";
4+import { timeAgo } from "@/lib/format";
5+import TreeTable from "@/components/TreeTable";
6+import Markdown from "@/components/Markdown";
7+
8+export default async function RepoOverview({ params }: { params: Promise<{ repo: string }> }) {
9+ const name = dec((await params).repo);
10+ const repo = await getRepo(name);
11+ if (!repo) notFound();
12+
13+ if (repo.empty) {
14+ return (
15+ <div className="border border-line bg-panel px-6 py-10">
16+ <p className="font-mono text-sm text-gold">❄ an empty vessel</p>
17+ <p className="mt-3 text-sm text-fog">The owner hasn&apos;t pushed anything yet:</p>
18+ <pre className="mt-4 overflow-x-auto border border-line bg-abyss px-4 py-3 font-mono text-xs text-fog">
19+ {`git remote add frieren ${cloneUrl(repo.name)}\ngit push frieren ${repo.default}`}
20+ </pre>
21+ </div>
22+ );
23+ }
24+
25+ const [entries, readme] = await Promise.all([
26+ getTree(name, repo.default, ""),
27+ getReadme(name, repo.default),
28+ ]);
29+
30+ return (
31+ <div className="space-y-8">
32+ <p className="font-mono text-xs text-dim">
33+ <span className="text-gold/70">▪</span> {repo.default} · last commit{" "}
34+ {timeAgo(repo.lastCommit)}
35+ </p>
36+ <TreeTable repo={repo.name} refName={repo.default} dir="" entries={entries ?? []} />
37+ {readme && (
38+ <section>
39+ <h2 className="mb-3 border-b border-line pb-2 font-mono text-xs text-dim">
40+ {readme.name}
41+ </h2>
42+ <Markdown repo={repo.name} refName={repo.default} source={readme.content} />
43+ </section>
44+ )}
45+ </div>
46+ );
47+}
web/app/[repo]/refs/page.tsx+620
@@ -0,0 +1,62 @@
1+import Link from "next/link";
2+import { notFound } from "next/navigation";
3+import { getRefs } from "@/lib/api";
4+import { dec } from "@/lib/params";
5+import { timeAgo } from "@/lib/format";
6+
7+type Props = { params: Promise<{ repo: string }> };
8+
9+function RefList({
10+ repo,
11+ kind,
12+ refs,
13+}: {
14+ repo: string;
15+ kind: "branch" | "tag";
16+ refs: { name: string; short: string; when: string; subject: string }[];
17+}) {
18+ return (
19+ <section>
20+ <h2 className="mb-3 font-mono text-xs text-dim">
21+ {kind === "branch" ? "branches" : "tags"}
22+ </h2>
23+ {refs.length === 0 ? (
24+ <p className="text-sm text-dim">none</p>
25+ ) : (
26+ <div className="border border-line bg-panel">
27+ {refs.map((r) => (
28+ <div
29+ key={r.name}
30+ className="flex items-baseline gap-4 border-b border-line px-4 py-2.5 text-sm last:border-b-0"
31+ >
32+ {kind === "branch" ? (
33+ <Link
34+ href={`/${repo}/tree/${encodeURIComponent(r.name)}`}
35+ className="shrink-0 font-mono text-frost hover:underline"
36+ >
37+ {r.name}
38+ </Link>
39+ ) : (
40+ <span className="shrink-0 font-mono text-gold/90">{r.name}</span>
41+ )}
42+ <span className="min-w-0 flex-1 truncate text-fog">{r.subject}</span>
43+ <span className="shrink-0 font-mono text-xs text-dim">{timeAgo(r.when)}</span>
44+ </div>
45+ ))}
46+ </div>
47+ )}
48+ </section>
49+ );
50+}
51+
52+export default async function RefsPage({ params }: Props) {
53+ const repo = dec((await params).repo);
54+ const refs = await getRefs(repo);
55+ if (!refs) notFound();
56+ return (
57+ <div className="space-y-8">
58+ <RefList repo={repo} kind="branch" refs={refs.branches} />
59+ <RefList repo={repo} kind="tag" refs={refs.tags} />
60+ </div>
61+ );
62+}
web/app/[repo]/tree/[ref]/[[...path]]/page.tsx+230
@@ -0,0 +1,23 @@
1+import { notFound } from "next/navigation";
2+import { getTree } from "@/lib/api";
3+import { dec, decPath } from "@/lib/params";
4+import TreeTable from "@/components/TreeTable";
5+import Crumbs from "@/components/Crumbs";
6+
7+type Props = { params: Promise<{ repo: string; ref: string; path?: string[] }> };
8+
9+export default async function TreePage({ params }: Props) {
10+ const p = await params;
11+ const repo = dec(p.repo);
12+ const ref = dec(p.ref);
13+ const path = decPath(p.path);
14+ const entries = await getTree(repo, ref, path);
15+ if (!entries) notFound();
16+
17+ return (
18+ <div className="space-y-4">
19+ <Crumbs repo={repo} refName={ref} path={path} leafIsLink={false} />
20+ <TreeTable repo={repo} refName={ref} dir={path} entries={entries} />
21+ </div>
22+ );
23+}
web/app/error.tsx+200
@@ -0,0 +1,20 @@
1+"use client";
2+
3+export default function Error({ reset }: { error: Error; reset: () => void }) {
4+ return (
5+ <div className="mx-auto max-w-xl border border-line bg-panel px-8 py-10">
6+ <p className="font-mono text-sm text-gold">❄ the archive is unreachable</p>
7+ <p className="mt-4 text-sm leading-relaxed text-fog">
8+ The backend git server didn&apos;t answer. It may be waking up, restarting, or
9+ offline — the repositories themselves are safe on the owner&apos;s machine.
10+ </p>
11+ <button
12+ type="button"
13+ onClick={reset}
14+ className="mt-6 border border-line px-4 py-1.5 font-mono text-xs text-fog transition-colors hover:border-frost/60 hover:text-snow"
15+ >
16+ try again
17+ </button>
18+ </div>
19+ );
20+}
web/app/globals.css+1300
@@ -0,0 +1,130 @@
1+@import "tailwindcss";
2+
3+@theme inline {
4+ --color-night: #0b0c10;
5+ --color-abyss: #08090c;
6+ --color-panel: #101218;
7+ --color-raise: #161923;
8+ --color-line: #1f232e;
9+ --color-fog: #8a93a3;
10+ --color-dim: #5c6472;
11+ --color-snow: #dfe3ea;
12+ --color-frost: #5eead4;
13+ --color-gold: #e8c170;
14+ --color-add: #86efac;
15+ --color-add-bg: #0d2818;
16+ --color-del: #fca5a5;
17+ --color-del-bg: #2d1214;
18+ --font-body: var(--font-inter), system-ui, sans-serif;
19+ --font-mono: var(--font-jbmono), ui-monospace, monospace;
20+ --font-display: var(--font-serif), Georgia, serif;
21+}
22+
23+html {
24+ background: var(--color-night);
25+ color-scheme: dark;
26+}
27+
28+body {
29+ background: var(--color-night);
30+ color: var(--color-snow);
31+ font-family: var(--font-body);
32+ -webkit-font-smoothing: antialiased;
33+}
34+
35+::selection {
36+ background: color-mix(in srgb, var(--color-frost) 30%, transparent);
37+}
38+
39+/* ——— code blocks ——— */
40+
41+.shiki {
42+ background: transparent !important;
43+ counter-reset: ln;
44+ font-size: 0.8125rem;
45+ line-height: 1.6;
46+}
47+
48+.shiki code {
49+ display: block;
50+ width: fit-content;
51+ min-width: 100%;
52+}
53+
54+.shiki .line::before {
55+ counter-increment: ln;
56+ content: counter(ln);
57+ display: inline-block;
58+ width: 3.5ch;
59+ margin-right: 2ch;
60+ text-align: right;
61+ color: var(--color-dim);
62+ user-select: none;
63+}
64+
65+/* ——— rendered markdown ——— */
66+
67+.prose-frost {
68+ font-size: 0.9375rem;
69+ line-height: 1.7;
70+ color: var(--color-snow);
71+}
72+.prose-frost h1,
73+.prose-frost h2,
74+.prose-frost h3,
75+.prose-frost h4 {
76+ font-family: var(--font-display);
77+ font-weight: 400;
78+ letter-spacing: 0.01em;
79+ margin: 1.6em 0 0.5em;
80+ color: var(--color-snow);
81+}
82+.prose-frost h1 { font-size: 1.75rem; margin-top: 0.4em; }
83+.prose-frost h2 { font-size: 1.4rem; border-bottom: 1px solid var(--color-line); padding-bottom: 0.3em; }
84+.prose-frost h3 { font-size: 1.15rem; }
85+.prose-frost p { margin: 0.8em 0; }
86+.prose-frost a { color: var(--color-frost); }
87+.prose-frost a:hover { text-decoration: underline; }
88+.prose-frost code {
89+ font-family: var(--font-mono);
90+ font-size: 0.85em;
91+ background: var(--color-raise);
92+ border: 1px solid var(--color-line);
93+ padding: 0.1em 0.35em;
94+}
95+.prose-frost pre {
96+ background: var(--color-abyss);
97+ border: 1px solid var(--color-line);
98+ padding: 0.9rem 1.1rem;
99+ overflow-x: auto;
100+ margin: 1em 0;
101+ font-size: 0.8125rem;
102+ line-height: 1.6;
103+}
104+.prose-frost pre code { background: none; border: 0; padding: 0; font-size: inherit; }
105+.prose-frost ul, .prose-frost ol { padding-left: 1.4em; margin: 0.8em 0; }
106+.prose-frost ul { list-style: none; }
107+.prose-frost ul > li::before {
108+ content: "▪";
109+ color: var(--color-frost);
110+ display: inline-block;
111+ width: 1.2em;
112+ margin-left: -1.2em;
113+ font-size: 0.7em;
114+ vertical-align: 0.15em;
115+}
116+.prose-frost ol { list-style: decimal; }
117+.prose-frost li { margin: 0.3em 0; }
118+.prose-frost blockquote {
119+ border-left: 2px solid var(--color-gold);
120+ padding-left: 1em;
121+ color: var(--color-fog);
122+ margin: 1em 0;
123+}
124+.prose-frost table { border-collapse: collapse; margin: 1em 0; font-size: 0.875rem; }
125+.prose-frost th, .prose-frost td { border: 1px solid var(--color-line); padding: 0.4em 0.8em; text-align: left; }
126+.prose-frost th { background: var(--color-raise); font-weight: 600; }
127+.prose-frost img { max-width: 100%; border: 1px solid var(--color-line); }
128+.prose-frost hr { border: 0; border-top: 1px solid var(--color-line); margin: 1.6em 0; }
129+.prose-frost [align="center"] { text-align: center; }
130+.prose-frost [align="center"] img { display: inline-block; }
web/app/icon.svg+120
@@ -0,0 +1,12 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2+ <rect width="32" height="32" fill="#0b0c10"/>
3+ <g fill="#5eead4" shape-rendering="crispEdges">
4+ <rect x="14" y="4" width="4" height="24"/>
5+ <rect x="4" y="14" width="24" height="4"/>
6+ <rect x="8" y="8" width="4" height="4"/>
7+ <rect x="20" y="8" width="4" height="4"/>
8+ <rect x="8" y="20" width="4" height="4"/>
9+ <rect x="20" y="20" width="4" height="4"/>
10+ </g>
11+ <rect x="14" y="14" width="4" height="4" fill="#e8c170" shape-rendering="crispEdges"/>
12+</svg>
web/app/layout.tsx+460
@@ -0,0 +1,46 @@
1+import type { Metadata } from "next";
2+import { Inter, JetBrains_Mono, Instrument_Serif } from "next/font/google";
3+import Link from "next/link";
4+import Starfield from "@/components/Starfield";
5+import "./globals.css";
6+
7+const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
8+const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-jbmono" });
9+const serif = Instrument_Serif({
10+ subsets: ["latin"],
11+ weight: "400",
12+ style: ["normal", "italic"],
13+ variable: "--font-serif",
14+});
15+
16+export const metadata: Metadata = {
17+ title: { default: "frieren", template: "%s · frieren" },
18+ description:
19+ "A self-hosted archive of one person's code — browse and clone everything, write nothing.",
20+};
21+
22+export default function RootLayout({ children }: { children: React.ReactNode }) {
23+ return (
24+ <html lang="en" className={`${inter.variable} ${mono.variable} ${serif.variable}`}>
25+ <body className="relative min-h-screen">
26+ <Starfield />
27+ <header className="mx-auto flex max-w-5xl items-baseline justify-between px-6 pb-4 pt-6">
28+ <Link href="/" className="font-display text-2xl tracking-wide">
29+ <span className="text-frost">❄</span> frieren
30+ </Link>
31+ <span className="font-mono text-xs text-dim">a personal git archive</span>
32+ </header>
33+ <main className="mx-auto max-w-5xl px-6 pb-24 pt-6">{children}</main>
34+ <footer className="mx-auto max-w-5xl border-t border-line px-6 py-6 font-mono text-xs text-dim">
35+ one writer · world readers — served from the owner&apos;s own machine by{" "}
36+ <a
37+ href="https://github.com/justin06lee/frieren"
38+ className="text-fog hover:text-frost"
39+ >
40+ frieren
41+ </a>
42+ </footer>
43+ </body>
44+ </html>
45+ );
46+}
web/app/not-found.tsx+150
@@ -0,0 +1,15 @@
1+import Link from "next/link";
2+
3+export default function NotFound() {
4+ return (
5+ <div className="mx-auto max-w-xl border border-line bg-panel px-8 py-10 text-center">
6+ <p className="font-display text-4xl">404</p>
7+ <p className="mt-3 text-sm text-fog">
8+ Whatever was here, time has taken it.{" "}
9+ <Link href="/" className="text-frost hover:underline">
10+ Back to the archive.
11+ </Link>
12+ </p>
13+ </div>
14+ );
15+}
web/app/page.tsx+630
@@ -0,0 +1,63 @@
1+import Link from "next/link";
2+import { getRepos, BackendOffline, type RepoInfo } from "@/lib/api";
3+import { timeAgo } from "@/lib/format";
4+import Offline from "@/components/Offline";
5+
6+// Always render against the live backend — never bake an offline state in at build time.
7+export const dynamic = "force-dynamic";
8+
9+export default async function Home() {
10+ let repos: RepoInfo[];
11+ try {
12+ repos = (await getRepos()) ?? [];
13+ } catch (e) {
14+ if (e instanceof BackendOffline) return <Offline />;
15+ throw e;
16+ }
17+
18+ return (
19+ <>
20+ <section className="mb-14 mt-6">
21+ <h1 className="font-display text-5xl leading-tight">
22+ Code that <em className="text-frost">outlives</em> the platforms.
23+ </h1>
24+ <p className="mt-4 max-w-2xl text-fog">
25+ Every repository here lives on hardware its owner controls. Browse anything,
26+ clone everything — writing is reserved for one person.
27+ </p>
28+ </section>
29+
30+ {repos.length === 0 ? (
31+ <p className="border border-line bg-panel px-6 py-10 text-sm text-fog">
32+ The archive is empty — the first <code className="text-snow">git push</code> will
33+ fill it.
34+ </p>
35+ ) : (
36+ <div className="grid gap-4 sm:grid-cols-2">
37+ {repos.map((r) => (
38+ <Link
39+ key={r.name}
40+ href={`/${r.name}`}
41+ className="group border border-line bg-panel p-5 transition-colors hover:border-frost/60"
42+ >
43+ <div className="flex items-baseline justify-between gap-3">
44+ <h2 className="font-mono text-base text-snow group-hover:text-frost">
45+ {r.name}
46+ </h2>
47+ <span className="shrink-0 font-mono text-xs text-dim">
48+ {r.empty ? "empty" : timeAgo(r.lastCommit)}
49+ </span>
50+ </div>
51+ <p className="mt-2 line-clamp-2 min-h-10 text-sm text-fog">
52+ {r.description || "no description"}
53+ </p>
54+ <p className="mt-3 font-mono text-xs text-dim">
55+ <span className="text-gold/70">▪</span> {r.default}
56+ </p>
57+ </Link>
58+ ))}
59+ </div>
60+ )}
61+ </>
62+ );
63+}
web/bun.lock+4970
@@ -0,0 +1,497 @@
1+{
2+ "lockfileVersion": 1,
3+ "configVersion": 1,
4+ "workspaces": {
5+ "": {
6+ "name": "web",
7+ "dependencies": {
8+ "next": "16.3.1",
9+ "react": "19.2.8",
10+ "react-dom": "19.2.8",
11+ "react-markdown": "^10.1.0",
12+ "rehype-raw": "^7.0.0",
13+ "rehype-sanitize": "^6.0.0",
14+ "remark-gfm": "^4.0.1",
15+ "shiki": "^4.4.3",
16+ },
17+ "devDependencies": {
18+ "@tailwindcss/postcss": "^4",
19+ "@types/node": "^20",
20+ "@types/react": "^19",
21+ "@types/react-dom": "^19",
22+ "tailwindcss": "^4",
23+ "typescript": "^5",
24+ },
25+ },
26+ },
27+ "trustedDependencies": [
28+ "sharp",
29+ ],
30+ "packages": {
31+ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
32+
33+ "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
34+
35+ "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
36+
37+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
38+
39+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="],
40+
41+ "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="],
42+
43+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="],
44+
45+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="],
46+
47+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="],
48+
49+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="],
50+
51+ "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="],
52+
53+ "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="],
54+
55+ "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="],
56+
57+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="],
58+
59+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="],
60+
61+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="],
62+
63+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="],
64+
65+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="],
66+
67+ "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="],
68+
69+ "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="],
70+
71+ "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="],
72+
73+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="],
74+
75+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="],
76+
77+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="],
78+
79+ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="],
80+
81+ "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="],
82+
83+ "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="],
84+
85+ "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="],
86+
87+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
88+
89+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
90+
91+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
92+
93+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
94+
95+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
96+
97+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
98+
99+ "@next/env": ["@next/env@16.3.1", "", {}, "sha512-35G3xwkQUb2oETSDjFXGrVugknoayLFBh7vSE+yAcl9IP2zT9wyGwq7297AYHR11kJld807t5f8AJBs6WBzXsQ=="],
100+
101+ "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ABMIu2zQ7cnNIHm5ivKGwZwUrm0pAai3yiJ/gK/rF1c1VP9UOnj7XECbMKFdVKp9I9eMYq9NoDs1WXOoowxzJw=="],
102+
103+ "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gNG21e/UnrroeScbY/QndUEdl0mF1FRibW7BBeYUz/5ABCepjqDdEdgr592vpzMtCn/m7FTjYq3TN4TpyDnutw=="],
104+
105+ "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-6B6Lw016iwNUQuaJoraMMTLh6TwHzFUtxipSScD1F3YyymcrRWkobodRS2ftIOkF5vrs4zNlyUrTC5YZQ9Lz5w=="],
106+
107+ "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JUiPXZKK9wOhjf4MgDiH29GZLxfqOesbLtHq2pDxwH/WwscTRV2ToymnOTh1egzaZf0ueUf8T2+CeYTGHjW0Iw=="],
108+
109+ "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Uog9jsrmIRIL/lfvIp9htmskSNC7JcQsMVucXL2V2YY1y/D9IUN3LPEafqy0zRJ2cIU1SQ0V6F6TlffQ+pLAGg=="],
110+
111+ "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-6yy3FT13KgUFOj5H8bl8w/6nKiJwHIvbtwh1V+1acsu+7y4tJjnemSa6mhsh53BeoVrlozE+fMgZhXH46WmjMA=="],
112+
113+ "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-iOoN1QecUoGNZik536U/vtK43YwgyrCsGIkth52yIkl612n+0C9MjSnJbQAikISpb+WYRooBVhaDlUW7iZoKog=="],
114+
115+ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d/k+PpAriUPaeMJJOG7HUSdqfEX46FEPWU1p3/nm2ACmXhj9hFEWdFODUBIpkuijXYkfL90qZzTqVPRp4BW/hw=="],
116+
117+ "@shikijs/core": ["@shikijs/core@4.4.3", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="],
118+
119+ "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="],
120+
121+ "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="],
122+
123+ "@shikijs/langs": ["@shikijs/langs@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="],
124+
125+ "@shikijs/primitive": ["@shikijs/primitive@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="],
126+
127+ "@shikijs/themes": ["@shikijs/themes@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="],
128+
129+ "@shikijs/types": ["@shikijs/types@4.4.3", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="],
130+
131+ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
132+
133+ "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
134+
135+ "@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=="],
136+
137+ "@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=="],
138+
139+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
140+
141+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
142+
143+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
144+
145+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
146+
147+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
148+
149+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
150+
151+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
152+
153+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
154+
155+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
156+
157+ "@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=="],
158+
159+ "@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=="],
160+
161+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
162+
163+ "@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=="],
164+
165+ "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
166+
167+ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
168+
169+ "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
170+
171+ "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
172+
173+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
174+
175+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
176+
177+ "@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
178+
179+ "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
180+
181+ "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="],
182+
183+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
184+
185+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
186+
187+ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
188+
189+ "baseline-browser-mapping": ["baseline-browser-mapping@2.11.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="],
190+
191+ "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="],
192+
193+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
194+
195+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
196+
197+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
198+
199+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
200+
201+ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
202+
203+ "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
204+
205+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
206+
207+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
208+
209+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
210+
211+ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
212+
213+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
214+
215+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
216+
217+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
218+
219+ "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
220+
221+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
222+
223+ "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
224+
225+ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
226+
227+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
228+
229+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
230+
231+ "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
232+
233+ "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
234+
235+ "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
236+
237+ "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
238+
239+ "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=="],
240+
241+ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
242+
243+ "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
244+
245+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
246+
247+ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
248+
249+ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
250+
251+ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
252+
253+ "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
254+
255+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
256+
257+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
258+
259+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
260+
261+ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
262+
263+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
264+
265+ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
266+
267+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
268+
269+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
270+
271+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
272+
273+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
274+
275+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
276+
277+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
278+
279+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
280+
281+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
282+
283+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
284+
285+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
286+
287+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
288+
289+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
290+
291+ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
292+
293+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
294+
295+ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
296+
297+ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
298+
299+ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
300+
301+ "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
302+
303+ "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
304+
305+ "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
306+
307+ "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
308+
309+ "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
310+
311+ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
312+
313+ "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
314+
315+ "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
316+
317+ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
318+
319+ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
320+
321+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
322+
323+ "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
324+
325+ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
326+
327+ "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
328+
329+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
330+
331+ "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
332+
333+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
334+
335+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
336+
337+ "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
338+
339+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
340+
341+ "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
342+
343+ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
344+
345+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
346+
347+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
348+
349+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
350+
351+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
352+
353+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
354+
355+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
356+
357+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
358+
359+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
360+
361+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
362+
363+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
364+
365+ "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
366+
367+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
368+
369+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
370+
371+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
372+
373+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
374+
375+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
376+
377+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
378+
379+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
380+
381+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
382+
383+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
384+
385+ "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
386+
387+ "next": ["next@16.3.1", "", { "dependencies": { "@next/env": "16.3.1", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.1", "@next/swc-darwin-x64": "16.3.1", "@next/swc-linux-arm64-gnu": "16.3.1", "@next/swc-linux-arm64-musl": "16.3.1", "@next/swc-linux-x64-gnu": "16.3.1", "@next/swc-linux-x64-musl": "16.3.1", "@next/swc-win32-arm64-msvc": "16.3.1", "@next/swc-win32-x64-msvc": "16.3.1", "sharp": "^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hsAp0i7Rh+/dhe7DGIeN2YlpLM1DP4MNxti9EtDMtqcO612X81MvvEj388/oTce9U1EcEIOWDlGq0zRwrBKvuA=="],
388+
389+ "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
390+
391+ "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
392+
393+ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
394+
395+ "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
396+
397+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
398+
399+ "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
400+
401+ "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
402+
403+ "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
404+
405+ "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
406+
407+ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
408+
409+ "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
410+
411+ "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
412+
413+ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
414+
415+ "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
416+
417+ "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
418+
419+ "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
420+
421+ "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
422+
423+ "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
424+
425+ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
426+
427+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
428+
429+ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
430+
431+ "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" } }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
432+
433+ "shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="],
434+
435+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
436+
437+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
438+
439+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
440+
441+ "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
442+
443+ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
444+
445+ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
446+
447+ "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
448+
449+ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
450+
451+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
452+
453+ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
454+
455+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
456+
457+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
458+
459+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
460+
461+ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
462+
463+ "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
464+
465+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
466+
467+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
468+
469+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
470+
471+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
472+
473+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
474+
475+ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
476+
477+ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
478+
479+ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
480+
481+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
482+
483+ "@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=="],
484+
485+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
486+
487+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
488+
489+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
490+
491+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
492+
493+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
494+
495+ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
496+ }
497+}
web/components/CloneBox.tsx+260
@@ -0,0 +1,26 @@
1+"use client";
2+
3+import { useState } from "react";
4+
5+export default function CloneBox({ url }: { url: string }) {
6+ const [copied, setCopied] = useState(false);
7+ const cmd = `git clone ${url}`;
8+ return (
9+ <button
10+ type="button"
11+ onClick={() => {
12+ navigator.clipboard.writeText(cmd).then(() => {
13+ setCopied(true);
14+ setTimeout(() => setCopied(false), 1500);
15+ });
16+ }}
17+ title="copy clone command"
18+ className="group flex max-w-full items-center gap-3 border border-line bg-panel px-3 py-1.5 text-left font-mono text-xs text-fog transition-colors hover:border-frost/60"
19+ >
20+ <span className="truncate">{cmd}</span>
21+ <span className={copied ? "text-frost" : "text-dim group-hover:text-frost"}>
22+ {copied ? "copied" : "copy"}
23+ </span>
24+ </button>
25+ );
26+}
web/components/Crumbs.tsx+410
@@ -0,0 +1,41 @@
1+import Link from "next/link";
2+
3+// Breadcrumb path for tree/blob pages: repo / dir / dir / name
4+export default function Crumbs({
5+ repo,
6+ refName,
7+ path,
8+ leafIsLink,
9+}: {
10+ repo: string;
11+ refName: string;
12+ path: string;
13+ leafIsLink: boolean;
14+}) {
15+ const ref = encodeURIComponent(refName);
16+ const parts = path === "" ? [] : path.split("/");
17+ return (
18+ <p className="font-mono text-sm text-fog">
19+ <Link href={`/${repo}/tree/${ref}`} className="text-snow hover:text-frost">
20+ {repo}
21+ </Link>
22+ {parts.map((part, i) => {
23+ const sub = parts.slice(0, i + 1).join("/");
24+ const last = i === parts.length - 1;
25+ return (
26+ <span key={sub}>
27+ {" / "}
28+ {last && !leafIsLink ? (
29+ <span className="text-snow">{part}</span>
30+ ) : (
31+ <Link href={`/${repo}/tree/${ref}/${sub}`} className="hover:text-frost">
32+ {part}
33+ </Link>
34+ )}
35+ </span>
36+ );
37+ })}
38+ <span className="ml-3 border border-line px-1.5 py-0.5 text-xs text-frost">{refName}</span>
39+ </p>
40+ );
41+}
web/components/Diff.tsx+560
@@ -0,0 +1,56 @@
1+import { parsePatch } from "@/lib/diff";
2+
3+const lineStyles = {
4+ add: "bg-add-bg text-add",
5+ del: "bg-del-bg text-del",
6+ ctx: "text-fog",
7+ hunk: "bg-raise text-frost/80",
8+} as const;
9+
10+export default function Diff({ patch }: { patch: string }) {
11+ const { stat, files } = parsePatch(patch);
12+ return (
13+ <div className="space-y-6">
14+ {stat && (
15+ <pre className="overflow-x-auto border border-line bg-panel px-4 py-3 font-mono text-xs leading-relaxed text-fog">
16+ {stat}
17+ </pre>
18+ )}
19+ {files.map((f) => (
20+ <section key={f.path} className="border border-line bg-panel">
21+ <header className="flex items-center gap-3 border-b border-line bg-raise px-4 py-2 font-mono text-xs">
22+ <span className="truncate text-snow">{f.path}</span>
23+ <span className="ml-auto shrink-0 text-add">+{f.adds}</span>
24+ <span className="shrink-0 text-del">−{f.dels}</span>
25+ </header>
26+ {f.binary ? (
27+ <p className="px-4 py-4 font-mono text-xs text-fog">binary file changed</p>
28+ ) : (
29+ <div className="overflow-x-auto">
30+ <table className="w-full border-collapse font-mono text-xs leading-relaxed">
31+ <tbody>
32+ {f.lines.map((l, i) => (
33+ <tr key={i} className={lineStyles[l.kind]}>
34+ <td className="w-10 select-none pr-2 text-right align-top text-dim">
35+ {l.old ?? ""}
36+ </td>
37+ <td className="w-10 select-none pr-3 text-right align-top text-dim">
38+ {l.new ?? ""}
39+ </td>
40+ <td className="w-4 select-none text-center align-top opacity-70">
41+ {l.kind === "add" ? "+" : l.kind === "del" ? "−" : ""}
42+ </td>
43+ <td className="whitespace-pre pr-4 align-top">
44+ {l.kind === "hunk" ? l.text : l.text || " "}
45+ </td>
46+ </tr>
47+ ))}
48+ </tbody>
49+ </table>
50+ </div>
51+ )}
52+ </section>
53+ ))}
54+ </div>
55+ );
56+}
web/components/Markdown.tsx+430
@@ -0,0 +1,43 @@
1+import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
2+import remarkGfm from "remark-gfm";
3+import rehypeRaw from "rehype-raw";
4+import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
5+import { rawUrl } from "@/lib/api";
6+
7+// READMEs often embed HTML (centered headers, <img>, <br>). Render it, but
8+// sanitized — repository content must never script against the site.
9+const schema = {
10+ ...defaultSchema,
11+ attributes: {
12+ ...defaultSchema.attributes,
13+ "*": [...(defaultSchema.attributes?.["*"] ?? []), "align", "width", "height"],
14+ },
15+};
16+
17+// Renders a repository README. Relative image/link targets are rewritten to
18+// the backend's raw endpoint so screenshots in READMEs just work.
19+export default function Markdown({
20+ repo,
21+ refName,
22+ source,
23+}: {
24+ repo: string;
25+ refName: string;
26+ source: string;
27+}) {
28+ const transform = (url: string) => {
29+ if (/^(https?:|mailto:|#|data:)/i.test(url)) return defaultUrlTransform(url);
30+ return rawUrl(repo, refName, url.replace(/^\.\//, ""));
31+ };
32+ return (
33+ <div className="prose-frost">
34+ <ReactMarkdown
35+ remarkPlugins={[remarkGfm]}
36+ rehypePlugins={[rehypeRaw, [rehypeSanitize, schema]]}
37+ urlTransform={transform}
38+ >
39+ {source}
40+ </ReactMarkdown>
41+ </div>
42+ );
43+}
web/components/Offline.tsx+310
@@ -0,0 +1,31 @@
1+import { apiBase } from "@/lib/api";
2+
3+// Shown while the backend git server isn't reachable yet.
4+export default function Offline() {
5+ const base = apiBase();
6+ return (
7+ <div className="mx-auto max-w-xl border border-line bg-panel px-8 py-10">
8+ <p className="font-mono text-sm text-gold">❄ the archive is unreachable</p>
9+ <p className="mt-4 text-sm leading-relaxed text-fog">
10+ {base ? (
11+ <>
12+ This site is configured to read from <code className="text-snow">{base}</code>,
13+ but that server didn&apos;t answer. If you run this frieren, check that the
14+ backend is up and reachable from the internet.
15+ </>
16+ ) : (
17+ <>
18+ No backend is configured yet. Set the <code className="text-snow">FRIEREN_API_URL</code>{" "}
19+ environment variable on this deployment to the public URL of your frieren git
20+ server (for example <code className="text-snow">https://git.example.com</code>),
21+ then redeploy.
22+ </>
23+ )}
24+ </p>
25+ <p className="mt-4 text-sm leading-relaxed text-fog">
26+ Everything here is read-only — the repositories live on the owner&apos;s own
27+ machine, and this page is just the window into them.
28+ </p>
29+ </div>
30+ );
31+}
web/components/RepoNav.tsx+460
@@ -0,0 +1,46 @@
1+"use client";
2+
3+import Link from "next/link";
4+import { usePathname } from "next/navigation";
5+
6+export default function RepoNav({
7+ repo,
8+ defaultBranch,
9+}: {
10+ repo: string;
11+ defaultBranch: string;
12+}) {
13+ const pathname = usePathname();
14+ const base = `/${repo}`;
15+ const tabs = [
16+ { label: "overview", href: base, active: pathname === base },
17+ {
18+ label: "files",
19+ href: `${base}/tree/${encodeURIComponent(defaultBranch)}`,
20+ active: pathname.startsWith(`${base}/tree/`) || pathname.startsWith(`${base}/blob/`),
21+ },
22+ {
23+ label: "commits",
24+ href: `${base}/commits`,
25+ active: pathname.startsWith(`${base}/commit`),
26+ },
27+ { label: "refs", href: `${base}/refs`, active: pathname === `${base}/refs` },
28+ ];
29+ return (
30+ <nav className="flex gap-6 border-b border-line font-mono text-sm">
31+ {tabs.map((t) => (
32+ <Link
33+ key={t.label}
34+ href={t.href}
35+ className={
36+ t.active
37+ ? "-mb-px border-b border-frost pb-2 text-snow"
38+ : "pb-2 text-fog transition-colors hover:text-snow"
39+ }
40+ >
41+ {t.label}
42+ </Link>
43+ ))}
44+ </nav>
45+ );
46+}
web/components/Starfield.tsx+340
@@ -0,0 +1,34 @@
1+// The banner's commit-graph constellation, faint, behind the page header.
2+export default function Starfield() {
3+ return (
4+ <svg
5+ aria-hidden
6+ className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-72 w-full"
7+ viewBox="0 0 1200 288"
8+ preserveAspectRatio="xMidYMin slice"
9+ >
10+ <g stroke="#1d2430" strokeWidth="1.5">
11+ <line x1="705" y1="165" x2="785" y2="145" />
12+ <line x1="785" y1="145" x2="865" y2="125" />
13+ <line x1="865" y1="125" x2="945" y2="105" />
14+ <line x1="785" y1="145" x2="845" y2="195" />
15+ <line x1="845" y1="195" x2="925" y2="175" />
16+ <line x1="925" y1="175" x2="945" y2="105" />
17+ </g>
18+ <g shapeRendering="crispEdges">
19+ <rect x="702" y="162" width="7" height="7" fill="#e8c170" opacity="0.55" />
20+ <rect x="782" y="142" width="7" height="7" fill="#e8c170" opacity="0.55" />
21+ <rect x="862" y="122" width="7" height="7" fill="#e8c170" opacity="0.55" />
22+ <rect x="942" y="102" width="7" height="7" fill="#e8c170" opacity="0.55" />
23+ <rect x="842" y="192" width="7" height="7" fill="#5eead4" opacity="0.55" />
24+ <rect x="922" y="172" width="7" height="7" fill="#5eead4" opacity="0.55" />
25+ {[
26+ [80, 60], [170, 130], [260, 40], [340, 100], [440, 60], [520, 150],
27+ [600, 50], [1020, 70], [1100, 150], [1160, 60], [90, 200], [420, 210],
28+ ].map(([x, y]) => (
29+ <rect key={`${x}-${y}`} x={x} y={y} width="3" height="3" fill="#2b3342" />
30+ ))}
31+ </g>
32+ </svg>
33+ );
34+}
web/components/TreeTable.tsx+630
@@ -0,0 +1,63 @@
1+import Link from "next/link";
2+import type { TreeEntry } from "@/lib/api";
3+import { byteSize } from "@/lib/format";
4+
5+function DirIcon() {
6+ return (
7+ <svg viewBox="0 0 16 16" className="h-3.5 w-3.5 fill-gold/80" aria-hidden shapeRendering="crispEdges">
8+ <path d="M1 3h5l1 2h8v8H1z" />
9+ </svg>
10+ );
11+}
12+
13+function FileIcon() {
14+ return (
15+ <svg viewBox="0 0 16 16" className="h-3.5 w-3.5 fill-dim" aria-hidden shapeRendering="crispEdges">
16+ <path d="M3 1h7l3 3v11H3z" />
17+ </svg>
18+ );
19+}
20+
21+export default function TreeTable({
22+ repo,
23+ refName,
24+ dir,
25+ entries,
26+}: {
27+ repo: string;
28+ refName: string;
29+ dir: string;
30+ entries: TreeEntry[];
31+}) {
32+ const ref = encodeURIComponent(refName);
33+ const base = dir ? `${dir}/` : "";
34+ return (
35+ <div className="border border-line bg-panel">
36+ {entries.map((e) => {
37+ const href = `/${repo}/${e.type === "tree" ? "tree" : "blob"}/${ref}/${base}${e.name}`;
38+ return (
39+ <div
40+ key={e.name}
41+ className="flex items-center gap-3 border-b border-line px-4 py-2 text-sm last:border-b-0 hover:bg-raise"
42+ >
43+ {e.type === "tree" ? <DirIcon /> : <FileIcon />}
44+ {e.type === "commit" ? (
45+ <span className="font-mono text-fog">{e.name} @ {e.hash.slice(0, 7)}</span>
46+ ) : (
47+ <Link href={href} className="font-mono text-snow hover:text-frost">
48+ {e.name}
49+ {e.type === "tree" ? "/" : ""}
50+ </Link>
51+ )}
52+ <span className="ml-auto font-mono text-xs text-dim">
53+ {e.type === "blob" ? byteSize(e.size) : ""}
54+ </span>
55+ </div>
56+ );
57+ })}
58+ {entries.length === 0 && (
59+ <p className="px-4 py-6 text-sm text-fog">empty directory</p>
60+ )}
61+ </div>
62+ );
63+}
web/lib/api.ts+960
@@ -0,0 +1,96 @@
1+// Server-side client for the frieren backend's read-only JSON API.
2+
3+export type RepoInfo = {
4+ name: string;
5+ description: string;
6+ default: string;
7+ lastCommit: string;
8+ empty: boolean;
9+};
10+
11+export type TreeEntry = {
12+ mode: string;
13+ type: "blob" | "tree" | "commit";
14+ hash: string;
15+ size: number;
16+ name: string;
17+};
18+
19+export type Blob = {
20+ path: string;
21+ size: number;
22+ binary: boolean;
23+ truncated: boolean;
24+ content: string;
25+};
26+
27+export type Readme = { name: string; content: string };
28+
29+export type Commit = {
30+ hash: string;
31+ short: string;
32+ author: string;
33+ when: string;
34+ subject: string;
35+};
36+
37+export type CommitDetail = Commit & { patch: string; truncated: boolean };
38+
39+export type Refs = {
40+ branches: { name: string; short: string; when: string; subject: string }[];
41+ tags: { name: string; short: string; when: string; subject: string }[];
42+};
43+
44+export function apiBase(): string | null {
45+ const base = process.env.FRIEREN_API_URL;
46+ return base ? base.replace(/\/+$/, "") : null;
47+}
48+
49+// Where git users point their clients — defaults to the API host.
50+export function cloneUrl(repo: string): string {
51+ const base = process.env.FRIEREN_CLONE_URL?.replace(/\/+$/, "") ?? apiBase();
52+ return `${base ?? "https://your-frieren-server"}/${repo}.git`;
53+}
54+
55+export function rawUrl(repo: string, ref: string, path: string): string {
56+ const segs = path.split("/").map(encodeURIComponent).join("/");
57+ return `${apiBase()}/${repo}/raw/${encodeURIComponent(ref)}/${segs}`;
58+}
59+
60+export class BackendOffline extends Error {
61+ constructor() {
62+ super("frieren backend unreachable");
63+ }
64+}
65+
66+// api fetches a path, returning null on 404/400 and throwing BackendOffline
67+// when the backend is missing or unreachable.
68+async function api<T>(path: string): Promise<T | null> {
69+ const base = apiBase();
70+ if (!base) throw new BackendOffline();
71+ let res: Response;
72+ try {
73+ res = await fetch(`${base}/api${path}`, { next: { revalidate: 30 } });
74+ } catch {
75+ throw new BackendOffline();
76+ }
77+ if (res.status === 404 || res.status === 400) return null;
78+ if (!res.ok) throw new BackendOffline();
79+ return (await res.json()) as T;
80+}
81+
82+const q = encodeURIComponent;
83+
84+export const getRepos = () => api<RepoInfo[]>("/repos");
85+export const getRepo = (repo: string) => api<RepoInfo>(`/repos/${q(repo)}`);
86+export const getTree = (repo: string, ref: string, path: string) =>
87+ api<TreeEntry[]>(`/repos/${q(repo)}/tree?ref=${q(ref)}&path=${q(path)}`);
88+export const getBlob = (repo: string, ref: string, path: string) =>
89+ api<Blob>(`/repos/${q(repo)}/blob?ref=${q(ref)}&path=${q(path)}`);
90+export const getReadme = (repo: string, ref: string) =>
91+ api<Readme>(`/repos/${q(repo)}/readme?ref=${q(ref)}`);
92+export const getCommits = (repo: string, ref: string, n = 100) =>
93+ api<Commit[]>(`/repos/${q(repo)}/commits?ref=${q(ref)}&n=${n}`);
94+export const getCommit = (repo: string, hash: string) =>
95+ api<CommitDetail>(`/repos/${q(repo)}/commit/${q(hash)}`);
96+export const getRefs = (repo: string) => api<Refs>(`/repos/${q(repo)}/refs`);
web/lib/diff.ts+710
@@ -0,0 +1,71 @@
1+// Parses the `git show --stat --patch` text the backend returns into a
2+// structure the diff view can render with per-side line numbers.
3+
4+export type DiffLine = {
5+ kind: "add" | "del" | "ctx" | "hunk";
6+ old: number | null;
7+ new: number | null;
8+ text: string;
9+};
10+
11+export type DiffFile = {
12+ path: string;
13+ adds: number;
14+ dels: number;
15+ binary: boolean;
16+ lines: DiffLine[];
17+};
18+
19+export type ParsedPatch = { stat: string; files: DiffFile[] };
20+
21+export function parsePatch(patch: string): ParsedPatch {
22+ const idx = patch.indexOf("diff --git ");
23+ const stat = (idx === -1 ? patch : patch.slice(0, idx)).trim();
24+ const body = idx === -1 ? "" : patch.slice(idx);
25+
26+ const files: DiffFile[] = [];
27+ let file: DiffFile | null = null;
28+ let oldN = 0;
29+ let newN = 0;
30+
31+ for (const line of body.split("\n")) {
32+ if (line.startsWith("diff --git ")) {
33+ // `diff --git a/path b/path` — take the b/ side.
34+ const m = line.match(/ b\/(.*)$/);
35+ file = { path: m ? m[1] : line, adds: 0, dels: 0, binary: false, lines: [] };
36+ files.push(file);
37+ continue;
38+ }
39+ if (!file) continue;
40+ if (line.startsWith("Binary files ")) {
41+ file.binary = true;
42+ continue;
43+ }
44+ const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
45+ if (hunk) {
46+ oldN = parseInt(hunk[1], 10);
47+ newN = parseInt(hunk[2], 10);
48+ file.lines.push({ kind: "hunk", old: null, new: null, text: line });
49+ continue;
50+ }
51+ if (
52+ line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ ") ||
53+ line.startsWith("new file") || line.startsWith("deleted file") ||
54+ line.startsWith("old mode") || line.startsWith("new mode") ||
55+ line.startsWith("similarity") || line.startsWith("rename ") ||
56+ line.startsWith("\\ No newline")
57+ ) {
58+ continue;
59+ }
60+ if (line.startsWith("+")) {
61+ file.adds++;
62+ file.lines.push({ kind: "add", old: null, new: newN++, text: line.slice(1) });
63+ } else if (line.startsWith("-")) {
64+ file.dels++;
65+ file.lines.push({ kind: "del", old: oldN++, new: null, text: line.slice(1) });
66+ } else {
67+ file.lines.push({ kind: "ctx", old: oldN++, new: newN++, text: line.slice(1) });
68+ }
69+ }
70+ return { stat, files };
71+}
web/lib/format.ts+310
@@ -0,0 +1,31 @@
1+export function timeAgo(iso: string): string {
2+ const t = new Date(iso).getTime();
3+ if (!t || t <= 0) return "";
4+ const s = (Date.now() - t) / 1000;
5+ if (s < 60) return "just now";
6+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
7+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
8+ if (s < 30 * 86400) return `${Math.floor(s / 86400)}d ago`;
9+ return new Date(iso).toLocaleDateString("en-US", {
10+ year: "numeric",
11+ month: "short",
12+ day: "numeric",
13+ });
14+}
15+
16+export function byteSize(n: number): string {
17+ if (n < 0) return "";
18+ if (n < 1024) return `${n} B`;
19+ if (n < 1 << 20) return `${(n / 1024).toFixed(1)} KB`;
20+ return `${(n / (1 << 20)).toFixed(1)} MB`;
21+}
22+
23+export function fullDate(iso: string): string {
24+ return new Date(iso).toLocaleString("en-US", {
25+ year: "numeric",
26+ month: "short",
27+ day: "numeric",
28+ hour: "2-digit",
29+ minute: "2-digit",
30+ });
31+}
web/lib/params.ts+130
@@ -0,0 +1,13 @@
1+// Next.js delivers dynamic segments percent-encoded in some cases; decode
2+// defensively so refs like "feat%2Fauth" become "feat/auth".
3+export function dec(s: string): string {
4+ try {
5+ return decodeURIComponent(s);
6+ } catch {
7+ return s;
8+ }
9+}
10+
11+export function decPath(segs: string[] | undefined): string {
12+ return (segs ?? []).map(dec).join("/");
13+}
web/lib/shiki.ts+460
@@ -0,0 +1,46 @@
1+import { createHighlighter, type Highlighter } from "shiki";
2+
3+const LANGS = [
4+ "typescript", "tsx", "javascript", "jsx", "json", "go", "rust", "python",
5+ "c", "cpp", "css", "html", "yaml", "toml", "markdown", "bash", "sql",
6+ "java", "swift", "kotlin", "ruby", "php", "docker", "make", "diff",
7+];
8+
9+const EXT_TO_LANG: Record<string, string> = {
10+ ts: "typescript", mts: "typescript", cts: "typescript", tsx: "tsx",
11+ js: "javascript", mjs: "javascript", cjs: "javascript", jsx: "jsx",
12+ json: "json", go: "go", rs: "rust", py: "python",
13+ c: "c", h: "c", cpp: "cpp", cc: "cpp", hpp: "cpp",
14+ css: "css", html: "html", htm: "html",
15+ yml: "yaml", yaml: "yaml", toml: "toml", md: "markdown",
16+ sh: "bash", bash: "bash", zsh: "bash", sql: "sql",
17+ java: "java", swift: "swift", kt: "kotlin", rb: "ruby", php: "php",
18+ dockerfile: "docker", patch: "diff", diff: "diff",
19+};
20+
21+let highlighter: Promise<Highlighter> | null = null;
22+
23+function getHighlighter(): Promise<Highlighter> {
24+ highlighter ??= createHighlighter({ themes: ["vitesse-dark"], langs: LANGS });
25+ return highlighter;
26+}
27+
28+export function langForFile(name: string): string | null {
29+ const base = name.toLowerCase().split("/").pop() ?? "";
30+ if (base === "makefile") return "make";
31+ if (base === "dockerfile") return "docker";
32+ const ext = base.includes(".") ? base.split(".").pop()! : "";
33+ return EXT_TO_LANG[ext] ?? null;
34+}
35+
36+// highlight returns shiki HTML for known languages, null otherwise
37+// (the caller renders a plain <pre> instead).
38+export async function highlight(code: string, lang: string | null): Promise<string | null> {
39+ if (!lang) return null;
40+ try {
41+ const hl = await getHighlighter();
42+ return hl.codeToHtml(code, { lang, theme: "vitesse-dark" });
43+ } catch {
44+ return null;
45+ }
46+}
web/next.config.ts+70
@@ -0,0 +1,7 @@
1+import type { NextConfig } from "next";
2+
3+const nextConfig: NextConfig = {
4+ /* config options here */
5+};
6+
7+export default nextConfig;
web/package.json+370
@@ -0,0 +1,37 @@
1+{
2+ "name": "web",
3+ "version": "0.1.0",
4+ "private": true,
5+ "scripts": {
6+ "dev": "next dev",
7+ "build": "next build",
8+ "start": "next start"
9+ },
10+ "dependencies": {
11+ "next": "16.3.1",
12+ "react": "19.2.8",
13+ "react-dom": "19.2.8",
14+ "react-markdown": "^10.1.0",
15+ "rehype-raw": "^7.0.0",
16+ "rehype-sanitize": "^6.0.0",
17+ "remark-gfm": "^4.0.1",
18+ "shiki": "^4.4.3"
19+ },
20+ "devDependencies": {
21+ "@tailwindcss/postcss": "^4",
22+ "@types/node": "^20",
23+ "@types/react": "^19",
24+ "@types/react-dom": "^19",
25+ "tailwindcss": "^4",
26+ "typescript": "^5"
27+ },
28+ "packageManager": "bun@1.3.14",
29+ "ignoreScripts": [
30+ "sharp",
31+ "unrs-resolver"
32+ ],
33+ "trustedDependencies": [
34+ "sharp",
35+ "unrs-resolver"
36+ ]
37+}
web/postcss.config.mjs+70
@@ -0,0 +1,7 @@
1+const config = {
2+ plugins: {
3+ "@tailwindcss/postcss": {},
4+ },
5+};
6+
7+export default config;
web/tsconfig.json+340
@@ -0,0 +1,34 @@
1+{
2+ "compilerOptions": {
3+ "target": "ES2017",
4+ "lib": ["dom", "dom.iterable", "esnext"],
5+ "allowJs": true,
6+ "skipLibCheck": true,
7+ "strict": true,
8+ "noEmit": true,
9+ "esModuleInterop": true,
10+ "module": "esnext",
11+ "moduleResolution": "bundler",
12+ "resolveJsonModule": true,
13+ "isolatedModules": true,
14+ "jsx": "react-jsx",
15+ "incremental": true,
16+ "plugins": [
17+ {
18+ "name": "next"
19+ }
20+ ],
21+ "paths": {
22+ "@/*": ["./*"]
23+ }
24+ },
25+ "include": [
26+ "next-env.d.ts",
27+ "**/*.ts",
28+ "**/*.tsx",
29+ ".next/types/**/*.ts",
30+ ".next/dev/types/**/*.ts",
31+ "**/*.mts"
32+ ],
33+ "exclude": ["node_modules"]
34+}
035