Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit b72c6f39707f3883b30efea3079587bb1740de4c
parent 5ae0fcf4aef52324e714d2c32b589afebf1007c4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 15 Apr 2026 13:54:36 -0400

Initialize the Jeralyzer

Diffstat:
M.gitignore | 9+++++++++
MREADME.md | 62++++++++++++++++++++++++++++++++++++++++----------------------
Aapp/PlayerProvider.tsx | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/TranscriptModal.tsx | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/TranscriptSearch.tsx | 389+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/layout.tsx | 29+++++++++++++++++++++++------
Mapp/page.tsx | 98+++++++++++++++++++++++++++++++++----------------------------------------------
Aapp/transcriptCache.ts | 21+++++++++++++++++++++
Aapp/transcripts/[slug]/route.ts | 15+++++++++++++++
Acreate-archives.sh | 9+++++++++
Alib/format.ts | 14++++++++++++++
Alib/settings.ts | 31+++++++++++++++++++++++++++++++
Alib/transcripts.ts | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/vtt.ts | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnext.config.ts | 4+++-
Mpackage.json | 9+++++++--
Mpnpm-lock.yaml | 868+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpnpm-workspace.yaml | 2++
Asettings.json | 5+++++
19 files changed, 2001 insertions(+), 88 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -39,3 +39,12 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# nested transcripts repo +/transcripts + +# public downloadable repo archives (generate with create-archives.sh) +/public/code.tar.gz +/public/code.tar.xz +/public/transcripts.tar.gz +/public/transcripts.tar.xz diff --git a/README.md b/README.md @@ -1,36 +1,54 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Jeralyzer -## Getting Started +This is [Next.js](https://nextjs.org) vibe-coded slop built to make it easy to browse auto-generated YouTube transcripts downloaded with yt-dlp. -First, run the development server: +There's basic white-labeling functionality with the `settings.json` file if you feel like using this code for another purpose. -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +## Adding Transcripts + +This repo hosts the code for the app, and transcripts are to be inserted in the "transcripts" directory at the top level (alongside this README). + +The file structure looks like this: + +``` +. +├── README.md +├── transcripts +│   └── data +│   ├── 20180627_wCLdlbEHmLE-Introducing_The_Incredible_Salt_Mine +│   │   ├── metadata.info.json +│   │   └── transcript.en.vtt +│   ├── 20180714_P1Lfy0rStkI-The_Incredible_Salt_Mine_Episode_001 +│   │   ├── metadata.info.json +│   │   └── transcript.en.vtt ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +The directory names could be anything, but are prefaced by date for easy lazy date sorting when reading the dir. +The `metadata.info.json` and `transcript.en.vtt` filenames are hard-coded for simplicity's sake. + +## Getting Started -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Make sure you have `pnpm` installed. Run these commands to build the static site and locally host it. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +```bash +pnpm run build +pnpm run start +``` -## Learn More +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -To learn more about Next.js, take a look at the following resources: +## Development -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +If you want to change the code, use the development server for hot-reloading and error messages. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```bash +pnpm run dev +``` -## Deploy on Vercel +## Downloading Transcripts -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +Use this command to download transcripts that end up in the expected format: -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +``` +yt-dlp --ignore-config --skip-download --restrict-filenames --write-info-json -o "subtitle:%(upload_date)s_%(id)s-%(fulltitle,title)s/transcript" -o "infojson:%(upload_date)s_%(id)s-%(fulltitle,title)s/metadata" --write-auto-subs -- <VIDEO_OR_PLAYLIST> +``` diff --git a/app/PlayerProvider.tsx b/app/PlayerProvider.tsx @@ -0,0 +1,211 @@ +"use client"; + +import dynamic from "next/dynamic"; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import type ReactPlayerType from "react-player"; +import { fetchTranscript } from "./transcriptCache"; + +const ReactPlayer = dynamic(() => import("react-player/youtube"), { + ssr: false, +}); + +export type Cue = { start: number; end: number; text: string }; + +export type TranscriptSummary = { + slug: string; + id: string; + title: string; + uploadDate: string; + date: string; + duration: string; + channel: string; +}; + +export type TranscriptData = TranscriptSummary & { + description?: string; + cues?: Cue[]; +}; + +type Status = "idle" | "loading" | "ready"; + +type PlayerState = { + activeSlug: string | null; + data: TranscriptData | null; + status: Status; + modalOpen: boolean; + playing: boolean; + currentTime: number; + openTranscript: (slug: string, start?: number) => void; + closeModal: () => void; + seekTo: (seconds: number) => void; +}; + +const Ctx = createContext<PlayerState | null>(null); + +export function usePlayer(): PlayerState { + const v = useContext(Ctx); + if (!v) throw new Error("usePlayer must be inside PlayerProvider"); + return v; +} + +export function PlayerProvider({ + children, + transcripts, +}: { + children: React.ReactNode; + transcripts: TranscriptSummary[]; +}) { + const [activeSlug, setActiveSlug] = useState<string | null>(null); + const [data, setData] = useState<TranscriptData | null>(null); + const [status, setStatus] = useState<Status>("idle"); + const [modalOpen, setModalOpen] = useState(false); + const [playing, setPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const playerRef = useRef<ReactPlayerType | null>(null); + const pendingSeekRef = useRef<number | null>(null); + const readyForSlugRef = useRef<string | null>(null); + + const bySlug = useMemo(() => { + const m = new Map<string, TranscriptSummary>(); + for (const t of transcripts) m.set(t.slug, t); + return m; + }, [transcripts]); + + const openTranscript = useCallback( + (slug: string, start?: number) => { + setModalOpen(true); + if (slug === activeSlug) { + if (typeof start === "number" && playerRef.current) { + playerRef.current.seekTo(start, "seconds"); + setPlaying(true); + } + return; + } + const summary = bySlug.get(slug); + if (!summary) return; + setActiveSlug(slug); + setData({ ...summary }); + setStatus("loading"); + readyForSlugRef.current = null; + pendingSeekRef.current = typeof start === "number" ? start : 0; + fetchTranscript(slug).then((full) => { + setActiveSlug((current) => { + if (current !== slug) return current; + setData({ ...summary, description: full.description, cues: full.cues }); + setStatus("ready"); + return current; + }); + }).catch(() => { + setActiveSlug((current) => { + if (current !== slug) return current; + setStatus("ready"); + return current; + }); + }); + }, + [activeSlug, bySlug], + ); + + const closeModal = useCallback(() => setModalOpen(false), []); + + const seekTo = useCallback((seconds: number) => { + if (playerRef.current) { + playerRef.current.seekTo(seconds, "seconds"); + setPlaying(true); + } else { + pendingSeekRef.current = seconds; + } + }, []); + + useEffect(() => { + if (!modalOpen) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") closeModal(); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [modalOpen, closeModal]); + + const handleReady = () => { + if ( + readyForSlugRef.current !== activeSlug && + pendingSeekRef.current !== null && + playerRef.current + ) { + playerRef.current.seekTo(pendingSeekRef.current, "seconds"); + setPlaying(true); + pendingSeekRef.current = null; + readyForSlugRef.current = activeSlug; + } + }; + + const value: PlayerState = { + activeSlug, + data, + status, + modalOpen, + playing, + currentTime, + openTranscript, + closeModal, + seekTo, + }; + + const playerMode: "hidden" | "mini" | "modal" = !activeSlug + ? "hidden" + : modalOpen + ? "modal" + : "mini"; + + return ( + <Ctx.Provider value={value}> + {children} + + {playerMode !== "hidden" && activeSlug && data && ( + <div + className={ + playerMode === "modal" + ? "fixed left-1/2 -translate-x-1/2 top-16 w-[min(92vw,960px)] aspect-video z-[60] rounded-lg overflow-hidden bg-black shadow-2xl" + : "fixed bottom-4 right-4 w-72 sm:w-96 aspect-video z-[60] rounded-lg overflow-hidden bg-black shadow-2xl ring-1 ring-black/40" + } + > + <ReactPlayer + ref={(p: ReactPlayerType | null) => { + playerRef.current = p; + }} + url={`https://www.youtube.com/watch?v=${data.id}`} + width="100%" + height="100%" + playing={playing} + controls + onReady={handleReady} + onPlay={() => setPlaying(true)} + onPause={() => setPlaying(false)} + onProgress={(s: { playedSeconds: number }) => + setCurrentTime(s.playedSeconds) + } + /> + {playerMode === "mini" && ( + <button + type="button" + aria-label="Reopen transcript" + onClick={() => setModalOpen(true)} + className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent text-white text-xs px-3 py-1.5 text-left hover:from-black/95" + > + <span className="font-medium line-clamp-1">{data.title}</span> + <span className="opacity-70">click to reopen</span> + </button> + )} + </div> + )} + </Ctx.Provider> + ); +} diff --git a/app/TranscriptModal.tsx b/app/TranscriptModal.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { usePlayer } from "./PlayerProvider"; +import { formatTimestamp } from "@/lib/vtt"; +import { formatDate } from "@/lib/format"; + +export default function TranscriptModal() { + const { + modalOpen, + activeSlug, + data, + status, + currentTime, + closeModal, + seekTo, + } = usePlayer(); + const activeRef = useRef<HTMLLIElement | null>(null); + const scrollRef = useRef<HTMLDivElement | null>(null); + + const cues = data?.cues ?? []; + const activeIndex = findActiveIndex(cues, currentTime); + + useEffect(() => { + if (!modalOpen) return; + activeRef.current?.scrollIntoView({ block: "nearest", behavior: "smooth" }); + }, [activeIndex, modalOpen]); + + useEffect(() => { + if (modalOpen) { + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = prev; + }; + } + }, [modalOpen]); + + if (!modalOpen || !activeSlug) return null; + + return ( + <div className="fixed inset-0 z-50 flex flex-col"> + <button + type="button" + aria-label="Close" + onClick={closeModal} + className="absolute inset-0 bg-black/70 backdrop-blur-sm" + /> + <div className="relative flex flex-col items-center w-full h-full overflow-hidden pt-4 pointer-events-none"> + <div + className="w-[min(92vw,960px)] pointer-events-auto flex flex-col gap-3" + style={{ + marginTop: "calc(min(92vw, 960px) * 9 / 16 + 3rem)", + maxHeight: + "calc(100vh - (min(92vw, 960px) * 9 / 16) - 5rem)", + }} + > + <div className="flex items-start justify-between gap-3 text-white"> + <div className="min-w-0"> + <h2 className="text-lg font-semibold leading-tight truncate"> + {data?.title ?? "Loading…"} + </h2> + {data && ( + <p className="text-xs text-zinc-300 mt-0.5"> + {formatDate(data.uploadDate)} + {data.channel && ` · ${data.channel}`} + </p> + )} + </div> + <button + type="button" + onClick={closeModal} + className="shrink-0 rounded-md bg-white/10 hover:bg-white/20 text-white px-3 py-1.5 text-sm" + > + Close + </button> + </div> + + <div + ref={scrollRef} + className="flex-1 overflow-y-auto rounded-lg border border-zinc-700 bg-zinc-900" + > + {status === "loading" && ( + <p className="text-sm text-zinc-400 p-4">Loading transcript…</p> + )} + {status === "ready" && cues.length === 0 && ( + <p className="text-sm text-zinc-400 p-4">No cues available.</p> + )} + {status === "ready" && cues.length > 0 && ( + <ol className="divide-y divide-zinc-800 text-zinc-100"> + {cues.map((cue, i) => { + const isActive = i === activeIndex; + return ( + <li + key={i} + ref={isActive ? activeRef : undefined} + className={ + isActive ? "bg-blue-950/50" : "" + } + > + <button + type="button" + onClick={() => seekTo(cue.start)} + className="w-full text-left flex gap-3 px-3 py-2 hover:bg-zinc-800" + > + <span className="text-xs font-mono text-zinc-400 shrink-0 w-16 pt-0.5"> + {formatTimestamp(cue.start)} + </span> + <span className="text-sm">{cue.text}</span> + </button> + </li> + ); + })} + </ol> + )} + </div> + </div> + </div> + </div> + ); +} + +function findActiveIndex( + cues: { start: number }[], + t: number, +): number { + if (!cues.length) return -1; + let lo = 0; + let hi = cues.length - 1; + let found = -1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (cues[mid].start <= t) { + found = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + return found; +} + diff --git a/app/TranscriptSearch.tsx b/app/TranscriptSearch.tsx @@ -0,0 +1,389 @@ +"use client"; + +import { useEffect, useMemo, useState, useSyncExternalStore } from "react"; +import { usePlayer } from "./PlayerProvider"; +import { fetchTranscript } from "./transcriptCache"; +import type { TranscriptDetail } from "@/lib/transcripts"; + +type Summary = { + slug: string; + id: string; + title: string; + uploadDate: string; + date: string; + duration: string; + channel: string; +}; + +type Hit = { + slug: string; + title: string; + date: string; + start: number; + text: string; +}; + +const MAX_HITS = 200; + +const urlListeners = new Set<() => void>(); +function subscribeUrl(cb: () => void) { + urlListeners.add(cb); + window.addEventListener("popstate", cb); + return () => { + urlListeners.delete(cb); + window.removeEventListener("popstate", cb); + }; +} +function getUrlSnapshot() { + return window.location.search; +} +function getServerUrlSnapshot() { + return ""; +} +function writeUrl(query: string, useRegex: boolean) { + const params = new URLSearchParams(window.location.search); + if (query) params.set("q", query); + else params.delete("q"); + if (useRegex) params.set("re", "1"); + else params.delete("re"); + const qs = params.toString(); + const next = `${window.location.pathname}${qs ? `?${qs}` : ""}`; + if (next === window.location.pathname + window.location.search) return; + window.history.replaceState(window.history.state, "", next); + for (const l of urlListeners) l(); +} + +export default function TranscriptSearch({ + transcripts, +}: { + transcripts: Summary[]; +}) { + const search = useSyncExternalStore( + subscribeUrl, + getUrlSnapshot, + getServerUrlSnapshot, + ); + const { query, useRegex } = useMemo(() => { + const params = new URLSearchParams(search); + return { + query: params.get("q") ?? "", + useRegex: params.get("re") === "1", + }; + }, [search]); + const setQuery = (q: string) => writeUrl(q, useRegex); + const setUseRegex = (r: boolean) => writeUrl(query, r); + const [lastHit, setLastHit] = useState<{ + slug: string; + start: number; + } | null>(null); + const [fetched, setFetched] = useState<Record<string, TranscriptDetail>>({}); + const { openTranscript } = usePlayer(); + + const trimmed = query.trim(); + + useEffect(() => { + if (!trimmed) return; + let cancelled = false; + for (const t of transcripts) { + if (fetched[t.slug]) continue; + fetchTranscript(t.slug) + .then((full) => { + if (cancelled) return; + setFetched((prev) => + prev[t.slug] ? prev : { ...prev, [t.slug]: full }, + ); + }) + .catch(() => {}); + } + return () => { + cancelled = true; + }; + }, [trimmed, transcripts, fetched]); + + const loadedCount = Object.keys(fetched).length; + const totalCount = transcripts.length; + const fetching = Boolean(trimmed) && loadedCount < totalCount; + + const regex = useMemo<{ re: RegExp | null; error: string | null }>(() => { + if (!trimmed || !useRegex) return { re: null, error: null }; + try { + return { re: new RegExp(trimmed, "i"), error: null }; + } catch (e) { + return { re: null, error: (e as Error).message }; + } + }, [trimmed, useRegex]); + + const matches = (haystack: string): boolean => { + if (!trimmed) return true; + if (useRegex) return regex.re ? regex.re.test(haystack) : false; + return haystack.toLowerCase().includes(trimmed.toLowerCase()); + }; + + const summaryBySlug = useMemo(() => { + const m = new Map<string, Summary>(); + for (const t of transcripts) m.set(t.slug, t); + return m; + }, [transcripts]); + + const titleMatches = useMemo(() => { + if (!trimmed) return transcripts; + if (useRegex && !regex.re) return []; + return transcripts.filter((t) => matches(t.title)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [trimmed, transcripts, useRegex, regex.re]); + + const cueHits = useMemo<Hit[]>(() => { + if (!trimmed) return []; + if (useRegex && !regex.re) return []; + const hits: Hit[] = []; + for (const t of transcripts) { + const full = fetched[t.slug]; + if (!full?.cues) continue; + for (const c of full.cues) { + if (matches(c.text)) { + hits.push({ + slug: t.slug, + title: t.title, + date: summaryBySlug.get(t.slug)?.date ?? "", + start: Math.round(c.start), + text: c.text, + }); + if (hits.length >= MAX_HITS) return hits; + } + } + } + return hits; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [trimmed, transcripts, fetched, summaryBySlug, useRegex, regex.re]); + + return ( + <div className="flex flex-col gap-6"> + <div className="flex flex-col gap-1"> + <input + type="search" + value={query} + onChange={(e) => setQuery(e.target.value)} + placeholder={ + useRegex + ? "Regex pattern (case-insensitive)…" + : "Search titles and transcripts..." + } + className={`w-full rounded-lg border ${ + regex.error + ? "border-red-500" + : "border-zinc-300 dark:border-zinc-700" + } bg-white dark:bg-zinc-900 px-4 py-3 text-base outline-none focus:ring-2 focus:ring-blue-500 ${ + useRegex ? "font-mono" : "" + }`} + /> + <label className="flex items-center gap-2 text-sm text-zinc-600 dark:text-zinc-400 select-none"> + <input + type="checkbox" + checked={useRegex} + onChange={(e) => setUseRegex(e.target.checked)} + className="accent-blue-600" + /> + Regex + {regex.error && ( + <span className="text-red-600 font-mono text-xs"> + {regex.error} + </span> + )} + </label> + </div> + + {!trimmed && ( + <TranscriptGrid + items={transcripts} + onOpen={(slug) => { + setLastHit({ slug, start: 0 }); + openTranscript(slug); + }} + lastSlug={lastHit?.slug ?? null} + /> + )} + + {trimmed && ( + <> + <section> + <h2 className="text-sm font-medium text-zinc-500 mb-2"> + Titles ({titleMatches.length}) + </h2> + {titleMatches.length > 0 ? ( + <TranscriptGrid + items={titleMatches} + onOpen={(slug) => { + setLastHit({ slug, start: 0 }); + openTranscript(slug); + }} + lastSlug={lastHit?.slug ?? null} + /> + ) : ( + <p className="text-sm text-zinc-500">No title matches.</p> + )} + </section> + + <section> + <h2 className="text-sm font-medium text-zinc-500 mb-2 flex items-center gap-2"> + <span> + Transcript mentions{" "} + {cueHits.length >= MAX_HITS + ? `(${MAX_HITS}+)` + : `(${cueHits.length})`} + </span> + {fetching && ( + <span className="text-xs text-zinc-400 font-normal"> + loaded {loadedCount}/{totalCount}… + </span> + )} + </h2> + {cueHits.length === 0 && !fetching && ( + <p className="text-sm text-zinc-500">No transcript mentions.</p> + )} + <ul className="flex flex-col divide-y divide-zinc-200 dark:divide-zinc-800 border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden bg-white dark:bg-zinc-900"> + {cueHits.map((h, i) => { + const isLast = + lastHit?.slug === h.slug && lastHit?.start === h.start; + return ( + <li key={i}> + <button + type="button" + onClick={() => { + setLastHit({ slug: h.slug, start: h.start }); + openTranscript(h.slug, h.start); + }} + className={`w-full text-left flex gap-3 px-4 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 ${ + isLast + ? "bg-amber-100/70 dark:bg-amber-500/10 ring-1 ring-inset ring-amber-400/60" + : "" + }`} + > + <span className="text-xs font-mono text-zinc-500 shrink-0 w-28"> + {formatSeconds(h.start)} + </span> + <span className="flex-1 min-w-0"> + <span className="block text-xs text-zinc-500 truncate"> + {h.title} + {h.date && ` · ${h.date}`} + </span> + <span className="block text-sm"> + {highlight(h.text, trimmed, useRegex)} + </span> + </span> + </button> + </li> + ); + })} + </ul> + </section> + </> + )} + </div> + ); +} + +function TranscriptGrid({ + items, + onOpen, + lastSlug, +}: { + items: Summary[]; + onOpen: (slug: string) => void; + lastSlug?: string | null; +}) { + return ( + <ul className="flex flex-col divide-y divide-zinc-200 dark:divide-zinc-800 border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden bg-white dark:bg-zinc-900"> + {items.map((t) => { + const isLast = lastSlug === t.slug; + return ( + <li key={t.slug}> + <button + type="button" + onClick={() => onOpen(t.slug)} + className={`w-full text-left flex items-baseline gap-3 px-4 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 ${ + isLast + ? "bg-amber-100/70 dark:bg-amber-500/10 ring-1 ring-inset ring-amber-400/60" + : "" + }`} + > + <span className="text-xs font-mono text-zinc-500 shrink-0 w-24"> + {t.date} + </span> + <span className="flex-1 min-w-0 font-medium truncate"> + {t.title} + </span> + {t.duration && ( + <span className="text-xs font-mono text-zinc-500 shrink-0"> + {t.duration} + </span> + )} + </button> + </li> + ); + })} + </ul> + ); +} + +function formatSeconds(s: number): string { + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + const mm = String(m).padStart(2, "0"); + const ss = String(sec).padStart(2, "0"); + return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`; +} + +function highlight(text: string, q: string, useRegex = false) { + if (!q) return text; + const parts: React.ReactNode[] = []; + + if (useRegex) { + let re: RegExp; + try { + re = new RegExp(q, "gi"); + } catch { + return text; + } + let last = 0; + let m: RegExpExecArray | null; + let key = 0; + while ((m = re.exec(text)) !== null) { + if (m.index > last) parts.push(text.slice(last, m.index)); + parts.push( + <mark + key={key++} + className="bg-yellow-200 dark:bg-yellow-500/30 text-inherit" + > + {m[0]} + </mark>, + ); + last = m.index + m[0].length; + if (m[0].length === 0) re.lastIndex++; + } + if (last < text.length) parts.push(text.slice(last)); + return <>{parts}</>; + } + + const lower = text.toLowerCase(); + const ql = q.toLowerCase(); + let i = 0; + while (i < text.length) { + const idx = lower.indexOf(ql, i); + if (idx === -1) { + parts.push(text.slice(i)); + break; + } + if (idx > i) parts.push(text.slice(i, idx)); + parts.push( + <mark + key={idx} + className="bg-yellow-200 dark:bg-yellow-500/30 text-inherit" + > + {text.slice(idx, idx + q.length)} + </mark>, + ); + i = idx + q.length; + } + return <>{parts}</>; +} diff --git a/app/layout.tsx b/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import Link from "next/link"; +import { getSettings } from "@/lib/settings"; import "./globals.css"; const geistSans = Geist({ @@ -12,22 +14,37 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; +export function generateMetadata(): Metadata { + const s = getSettings(); + return { + title: s.siteTitle, + description: s.siteDescription, + }; +} -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const settings = getSettings(); return ( <html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} > - <body className="min-h-full flex flex-col">{children}</body> + <body className="min-h-full flex flex-col bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100 font-sans"> + <header className="border-b border-zinc-200 dark:border-zinc-800 bg-white/70 dark:bg-zinc-900/70 backdrop-blur sticky top-0 z-20"> + <div className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between"> + <Link href="/" className="font-semibold tracking-tight"> + {settings.headerTitle} + </Link> + </div> + </header> + <main className="flex-1 w-full max-w-6xl mx-auto px-4 py-6"> + {children} + </main> + </body> </html> ); } diff --git a/app/page.tsx b/app/page.tsx @@ -1,65 +1,49 @@ -import Image from "next/image"; +import { + listTranscripts, + formatDate, + formatDuration, +} from "@/lib/transcripts"; +import { getSettings } from "@/lib/settings"; +import { PlayerProvider } from "./PlayerProvider"; +import TranscriptModal from "./TranscriptModal"; +import TranscriptSearch from "./TranscriptSearch"; + +export default async function Home() { + const settings = getSettings(); + const transcripts = await listTranscripts(); + const summaries = transcripts.map((t) => ({ + slug: t.slug, + id: t.id, + title: t.title, + uploadDate: t.uploadDate, + date: formatDate(t.uploadDate), + duration: formatDuration(t.duration), + channel: t.channel, + })); -export default function Home() { return ( - <div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black"> - <main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> - <Image - className="dark:invert" - src="/next.svg" - alt="Next.js logo" - width={100} - height={20} - priority - /> - <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left"> - <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> - To get started, edit the page.tsx file. + <PlayerProvider transcripts={summaries}> + <div className="flex flex-col gap-6"> + <div> + <h1 className="text-2xl font-semibold"> + {summaries.length} transcripts </h1> - <p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> - Looking for a starting point or more instructions? Head over to{" "} - <a - href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - className="font-medium text-zinc-950 dark:text-zinc-50" - > - Templates - </a>{" "} - or the{" "} - <a - href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - className="font-medium text-zinc-950 dark:text-zinc-50" - > - Learning - </a>{" "} - center. - </p> + {settings.homeTagline && ( + <p className="text-sm text-zinc-500 mt-1">{settings.homeTagline}</p> + )} </div> - <div className="flex flex-col gap-4 text-base font-medium sm:flex-row"> - <a - className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]" - href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - target="_blank" - rel="noopener noreferrer" - > - <Image - className="dark:invert" - src="/vercel.svg" - alt="Vercel logomark" - width={16} - height={16} - /> - Deploy Now + <TranscriptSearch transcripts={summaries} /> + <section className="border-t border-zinc-200 dark:border-zinc-800 pt-4 text-sm text-zinc-500 flex gap-4"> + <span>Downloads:</span> + <a href="/code.tar.gz" className="underline hover:text-zinc-900 dark:hover:text-zinc-100"> + code.tar.gz </a> - <a - className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]" - href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" - target="_blank" - rel="noopener noreferrer" - > - Documentation + <a href="/transcripts.tar.xz" className="underline hover:text-zinc-900 dark:hover:text-zinc-100"> + transcripts.tar.xz </a> - </div> - </main> - </div> + </section> + </div> + <TranscriptModal /> + </PlayerProvider> ); } diff --git a/app/transcriptCache.ts b/app/transcriptCache.ts @@ -0,0 +1,21 @@ +"use client"; + +import type { TranscriptDetail } from "@/lib/transcripts"; + +const cache = new Map<string, Promise<TranscriptDetail>>(); + +export function fetchTranscript(slug: string): Promise<TranscriptDetail> { + const hit = cache.get(slug); + if (hit) return hit; + const p = fetch(`/transcripts/${slug}`).then((r) => { + if (!r.ok) throw new Error(`Failed to fetch transcript ${slug}`); + return r.json() as Promise<TranscriptDetail>; + }); + cache.set(slug, p); + p.catch(() => cache.delete(slug)); + return p; +} + +export function peekTranscript(slug: string): Promise<TranscriptDetail> | undefined { + return cache.get(slug); +} diff --git a/app/transcripts/[slug]/route.ts b/app/transcripts/[slug]/route.ts @@ -0,0 +1,15 @@ +import { listSlugs, loadTranscript } from "@/lib/transcripts"; + +export const dynamic = "force-static"; + +export function generateStaticParams() { + return listSlugs().map((slug) => ({ slug })); +} + +export async function GET( + _req: Request, + { params }: { params: Promise<{ slug: string }> }, +) { + const { slug } = await params; + return Response.json(await loadTranscript(slug)); +} diff --git a/create-archives.sh b/create-archives.sh @@ -0,0 +1,9 @@ +#!/bin/bash +git archive -9 --format tar.gz -o "./public/code.tar.gz" main +cd transcripts + +# use xz over gz to get the file below 25MB + +# Uncommend this line to use .tar.gz for compatability and compression speed, but at a significantly higher file size +# git archive -9 --format tar.gz -o "../public/transcripts.tar.gz" main +git archive --format tar main | xz -9 > ../public/transcripts.tar.xz diff --git a/lib/format.ts b/lib/format.ts @@ -0,0 +1,14 @@ +export function formatDate(yyyymmdd: string): string { + if (!/^\d{8}$/.test(yyyymmdd)) return yyyymmdd; + return `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`; +} + +export function formatDuration(seconds: number): string { + if (!seconds) return ""; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + const mm = String(m).padStart(2, "0"); + const ss = String(s).padStart(2, "0"); + return h > 0 ? `${h}:${mm}:${ss}` : `${m}:${ss}`; +} diff --git a/lib/settings.ts b/lib/settings.ts @@ -0,0 +1,31 @@ +import fs from "node:fs"; +import path from "node:path"; + +export type SiteSettings = { + siteTitle: string; + siteDescription: string; + headerTitle: string; + homeTagline: string; +}; + +const DEFAULTS: SiteSettings = { + siteTitle: "Transcript Browser", + siteDescription: "Browse and search video transcripts", + headerTitle: "Transcript Browser", + homeTagline: "", +}; + +let cached: SiteSettings | null = null; + +export function getSettings(): SiteSettings { + if (cached) return cached; + const file = path.join(process.cwd(), "settings.json"); + let parsed: Partial<SiteSettings> = {}; + try { + parsed = JSON.parse(fs.readFileSync(file, "utf8")) as Partial<SiteSettings>; + } catch { + parsed = {}; + } + cached = { ...DEFAULTS, ...parsed }; + return cached; +} diff --git a/lib/transcripts.ts b/lib/transcripts.ts @@ -0,0 +1,86 @@ +import fs from "node:fs"; +import path from "node:path"; +import { parseVtt, type Cue } from "./vtt"; +import { readFile } from "fs-extra"; + +const ROOT = path.join(process.cwd(), "transcripts", "data"); + +export type TranscriptSummary = { + slug: string; + id: string; + title: string; + uploadDate: string; + duration: number; + channel: string; + description: string; +}; + +export type TranscriptDetail = TranscriptSummary & { + cues: Cue[] | undefined; +}; + +type RawMetadata = { + id?: string; + title?: string; + upload_date?: string; + duration?: number; + channel?: string; + uploader?: string; + description?: string; +}; + +async function readMetadata(slug: string): Promise<RawMetadata> { + const file = path.join(ROOT, slug, "metadata.info.json"); + const raw = await readFile(file, "utf8"); + return JSON.parse(raw) as RawMetadata; +} + +async function readTranscript(slug: string): Promise<Cue[] | undefined> { + const dir = path.join(ROOT, slug); + const vttPath = path.join(dir, "transcript.en.vtt"); + try { + const fileContents = await readFile(vttPath, "utf8"); + return parseVtt(fileContents); + } catch { + return undefined; + } +} + +function summarize(slug: string, meta: RawMetadata): TranscriptSummary { + const dateFromSlug = slug.match(/^(\d{8})_/)?.[1]; + return { + slug, + id: meta.id ?? slug.split("_")[1]?.split("-")[0] ?? slug, + title: meta.title ?? slug, + uploadDate: meta.upload_date ?? dateFromSlug ?? "", + duration: meta.duration ?? 0, + channel: meta.channel ?? meta.uploader ?? "", + description: meta.description ?? "", + }; +} + +export function listSlugs(): string[] { + return fs + .readdirSync(ROOT, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort(); +} + +export async function listTranscripts(): Promise<TranscriptSummary[]> { + return await Promise.all( + listSlugs().map(async (slug) => summarize(slug, await readMetadata(slug))), + ); +} + +export async function loadTranscript(slug: string): Promise<TranscriptDetail> { + const meta = await readMetadata(slug); + const cues = await readTranscript(slug); + return { ...summarize(slug, meta), cues }; +} + +export async function loadAllTranscripts(): Promise<TranscriptDetail[]> { + return Promise.all(listSlugs().map((slug) => loadTranscript(slug))); +} + +export { formatDate, formatDuration } from "./format"; diff --git a/lib/vtt.ts b/lib/vtt.ts @@ -0,0 +1,85 @@ +export type Cue = { start: number; end: number; text: string }; + +const TIMING_TAG_RE = /<\d{2}:\d{2}:\d{2}\.\d{3}>/; + +function parseTimestamp(ts: string): number { + const m = ts.match(/(\d+):(\d+):(\d+)\.(\d+)/); + if (!m) return 0; + const [, h, mm, s, ms] = m; + return +h * 3600 + +mm * 60 + +s + +ms / 1000; +} + +function decodeEntities(s: string): string { + return s + .replace(/&nbsp;/g, " ") + .replace(/&amp;/g, "&") + .replace(/&lt;/g, "<") + .replace(/&gt;/g, ">") + .replace(/&#39;/g, "'") + .replace(/&quot;/g, '"'); +} + +function stripTags(s: string): string { + return decodeEntities(s) + .replace(/<\d{2}:\d{2}:\d{2}\.\d{3}>/g, "") + .replace(/<\/?c[^>]*>/g, "") + .replace(/\s+/g, " ") + .trim(); +} + +export function parseVtt(src: string): Cue[] { + const lines = src.replace(/\r\n/g, "\n").split("\n"); + const cues: Cue[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + const m = line.match( + /(\d{2}:\d{2}:\d{2}\.\d{3})\s+-->\s+(\d{2}:\d{2}:\d{2}\.\d{3})/, + ); + if (!m) { + i++; + continue; + } + const start = parseTimestamp(m[1]); + const end = parseTimestamp(m[2]); + i++; + const body: string[] = []; + while (i < lines.length && lines[i].trim() !== "") { + body.push(lines[i]); + i++; + } + + const taggedLines = body.filter((l) => TIMING_TAG_RE.test(l)); + let text: string; + if (taggedLines.length > 0) { + text = stripTags(taggedLines[taggedLines.length - 1]); + } else { + continue; + } + + if (text) cues.push({ start, end, text }); + } + + const deduped: Cue[] = []; + for (const c of cues) { + const prev = deduped[deduped.length - 1]; + if (prev && prev.text === c.text) { + prev.end = c.end; + continue; + } + deduped.push({ ...c }); + } + + return deduped; +} + +export function formatTimestamp(seconds: number): string { + const s = Math.floor(seconds); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + const mm = String(m).padStart(2, "0"); + const ss = String(sec).padStart(2, "0"); + return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`; +} diff --git a/next.config.ts b/next.config.ts @@ -1,7 +1,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "export", + trailingSlash: true, + images: { unoptimized: true }, }; export default nextConfig; diff --git a/package.json b/package.json @@ -5,22 +5,27 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", + "start": "serve out", "lint": "eslint" }, "dependencies": { "next": "16.2.3", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "react-player": "^2.16.0", + "serve": "^14.2.6" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/fs-extra": "^11.0.4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.3", + "fs-extra": "^11.3.4", "tailwindcss": "^4", + "tsx": "^4.19.2", "typescript": "^5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml @@ -17,10 +17,19 @@ importers: react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) + react-player: + specifier: ^2.16.0 + version: 2.16.1(react@19.2.4) + serve: + specifier: ^14.2.6 + version: 14.2.6 devDependencies: '@tailwindcss/postcss': specifier: ^4 version: 4.2.2 + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 '@types/node': specifier: ^20 version: 20.19.39 @@ -36,9 +45,15 @@ importers: eslint-config-next: specifier: 16.2.3 version: 16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + fs-extra: + specifier: ^11.3.4 + version: 11.3.4 tailwindcss: specifier: ^4 version: 4.2.2 + tsx: + specifier: ^4.19.2 + version: 4.21.0 typescript: specifier: ^5 version: 5.9.3 @@ -125,6 +140,162 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -529,12 +700,18 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} @@ -708,6 +885,9 @@ packages: cpu: [x64] os: [win32] + '@zeit/schemas@2.36.0': + resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -721,10 +901,34 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -795,6 +999,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + boxen@7.0.0: + resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==} + engines: {node: '>=14.16'} + brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} @@ -811,6 +1019,14 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -827,16 +1043,36 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + caniuse-lite@1.0.30001788: resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.0.1: + resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clipboardy@3.0.0: + resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -844,9 +1080,21 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + content-disposition@0.5.2: + resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -872,6 +1120,14 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -889,9 +1145,17 @@ packages: supports-color: optional: true + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -912,9 +1176,15 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.336: resolution: {integrity: sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -954,6 +1224,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1082,6 +1357,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1095,6 +1374,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1130,6 +1412,15 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1156,6 +1447,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -1223,6 +1518,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1239,6 +1538,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -1278,6 +1580,11 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1286,6 +1593,10 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-generator-function@1.1.2: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} @@ -1310,6 +1621,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-port-reachable@4.0.0: + resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -1322,6 +1637,10 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -1346,6 +1665,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -1378,6 +1701,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -1390,6 +1716,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -1482,6 +1811,9 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + load-script@1.0.0: + resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1503,6 +1835,12 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1511,6 +1849,22 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.33.0: + resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.18: + resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1521,6 +1875,9 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1537,6 +1894,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + next@16.2.3: resolution: {integrity: sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==} engines: {node: '>=20.9.0'} @@ -1565,6 +1926,10 @@ packages: node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1597,6 +1962,14 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1621,6 +1994,9 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1628,6 +2004,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1665,14 +2044,30 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.2.0: + resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} + engines: {node: '>= 0.6'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: react: ^19.2.4 + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-player@2.16.1: + resolution: {integrity: sha512-mxP6CqjSWjidtyDoMOSHVPdhX0pY16aSvw5fVr44EMaT7X5Xz46uQ4b/YBm1v2x+3hHkB9PmjEEkmbHb9PXQ4w==} + peerDependencies: + react: '>=16.6.0' + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -1685,6 +2080,17 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + registry-auth-token@3.3.2: + resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==} + + registry-url@3.1.0: + resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1708,6 +2114,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -1728,6 +2137,14 @@ packages: engines: {node: '>=10'} hasBin: true + serve-handler@6.1.7: + resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} + + serve@14.2.6: + resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==} + engines: {node: '>= 14'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -1768,6 +2185,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1779,6 +2199,14 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -1802,10 +2230,26 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1858,10 +2302,19 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -1897,6 +2350,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -1906,9 +2363,16 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-check@1.5.4: + resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -1930,10 +2394,18 @@ packages: engines: {node: '>= 8'} hasBin: true + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2070,6 +2542,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -2376,10 +2926,19 @@ snapshots: '@types/estree@1.0.8': {} + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 20.19.39 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 20.19.39 + '@types/node@20.19.39': dependencies: undici-types: 6.21.0 @@ -2542,6 +3101,8 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@zeit/schemas@2.36.0': {} + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -2555,10 +3116,31 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + + arch@2.2.0: {} + + arg@5.0.2: {} + argparse@2.0.1: {} aria-query@5.3.2: {} @@ -2648,6 +3230,17 @@ snapshots: baseline-browser-mapping@2.10.19: {} + boxen@7.0.0: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.0.1 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 @@ -2669,6 +3262,10 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) + bytes@3.0.0: {} + + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -2688,23 +3285,57 @@ snapshots: callsites@3.1.0: {} + camelcase@7.0.1: {} + caniuse-lite@1.0.30001788: {} + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.0.1: {} + + cli-boxes@3.0.0: {} + client-only@0.0.1: {} + clipboardy@3.0.0: + dependencies: + arch: 2.2.0 + execa: 5.1.1 + is-wsl: 2.2.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + concat-map@0.0.1: {} + content-disposition@0.5.2: {} + convert-source-map@2.0.0: {} cross-spawn@7.0.6: @@ -2735,6 +3366,10 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -2743,8 +3378,12 @@ snapshots: dependencies: ms: 2.1.3 + deep-extend@0.6.0: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -2769,8 +3408,12 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.336: {} + emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} enhanced-resolve@5.20.1: @@ -2879,6 +3522,35 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -3088,6 +3760,18 @@ snapshots: esutils@2.0.3: {} + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -3102,6 +3786,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3134,6 +3820,15 @@ snapshots: dependencies: is-callable: 1.2.7 + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3169,6 +3864,8 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@6.0.1: {} + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -3228,6 +3925,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + human-signals@2.1.0: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3239,6 +3938,8 @@ snapshots: imurmurhash@0.1.4: {} + ini@1.3.8: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -3289,12 +3990,16 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-docker@2.2.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: dependencies: call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -3318,6 +4023,8 @@ snapshots: is-number@7.0.0: {} + is-port-reachable@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -3331,6 +4038,8 @@ snapshots: dependencies: call-bound: 1.0.4 + is-stream@2.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -3357,6 +4066,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isarray@2.0.5: {} isexe@2.0.0: {} @@ -3384,6 +4097,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -3392,6 +4107,12 @@ snapshots: json5@2.2.3: {} + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -3463,6 +4184,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + load-script@1.0.0: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -3483,6 +4206,10 @@ snapshots: math-intrinsics@1.1.0: {} + memoize-one@5.2.1: {} + + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -3490,6 +4217,16 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.33.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.18: + dependencies: + mime-db: 1.33.0 + + mimic-fn@2.1.0: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -3500,6 +4237,8 @@ snapshots: minimist@1.2.8: {} + ms@2.0.0: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -3508,6 +4247,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.4: {} + next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.3 @@ -3541,6 +4282,10 @@ snapshots: node-releases@2.0.37: {} + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -3583,6 +4328,12 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + on-headers@1.1.0: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3612,10 +4363,14 @@ snapshots: path-exists@4.0.0: {} + path-is-inside@1.0.2: {} + path-key@3.1.1: {} path-parse@1.0.7: {} + path-to-regexp@3.3.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -3648,13 +4403,33 @@ snapshots: queue-microtask@1.2.3: {} + range-parser@1.2.0: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 scheduler: 0.27.0 + react-fast-compare@3.2.2: {} + react-is@16.13.1: {} + react-player@2.16.1(react@19.2.4): + dependencies: + deepmerge: 4.3.1 + load-script: 1.0.0 + memoize-one: 5.2.1 + prop-types: 15.8.1 + react: 19.2.4 + react-fast-compare: 3.2.2 + react@19.2.4: {} reflect.getprototypeof@1.0.10: @@ -3677,6 +4452,17 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + registry-auth-token@3.3.2: + dependencies: + rc: 1.2.8 + safe-buffer: 5.2.1 + + registry-url@3.1.0: + dependencies: + rc: 1.2.8 + + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -3704,6 +4490,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -3721,6 +4509,32 @@ snapshots: semver@7.7.4: {} + serve-handler@6.1.7: + dependencies: + bytes: 3.0.0 + content-disposition: 0.5.2 + mime-types: 2.1.18 + minimatch: 3.1.5 + path-is-inside: 1.0.2 + path-to-regexp: 3.3.0 + range-parser: 1.2.0 + + serve@14.2.6: + dependencies: + '@zeit/schemas': 2.36.0 + ajv: 8.18.0 + arg: 5.0.2 + boxen: 7.0.0 + chalk: 5.0.1 + chalk-template: 0.4.0 + clipboardy: 3.0.0 + compression: 1.8.1 + is-port-reachable: 4.0.0 + serve-handler: 6.1.7 + update-check: 1.5.4 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -3809,6 +4623,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} + source-map-js@1.2.1: {} stable-hash@0.0.5: {} @@ -3818,6 +4634,18 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.9 @@ -3868,8 +4696,20 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-bom@3.0.0: {} + strip-final-newline@2.0.0: {} + + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): @@ -3911,10 +4751,19 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 + type-fest@2.19.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -3970,6 +4819,8 @@ snapshots: undici-types@6.21.0: {} + universalify@2.0.1: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -4000,10 +4851,17 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-check@1.5.4: + dependencies: + registry-auth-token: 3.3.2 + registry-url: 3.1.0 + uri-js@4.4.1: dependencies: punycode: 2.3.1 + vary@1.1.2: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -4049,8 +4907,18 @@ snapshots: dependencies: isexe: 2.0.0 + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + word-wrap@1.2.5: {} + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + yallist@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ ignoredBuiltDependencies: - sharp - unrs-resolver +onlyBuiltDependencies: + - esbuild diff --git a/settings.json b/settings.json @@ -0,0 +1,5 @@ +{ + "siteTitle": "Jeralyzer", + "siteDescription": "Browse and search Quartering's video transcripts", + "headerTitle": "Jeralyzer" +}