diff --git a/app/ecosystem/[slug]/page.tsx b/app/ecosystem/[slug]/page.tsx
new file mode 100644
index 0000000..5af9dc9
--- /dev/null
+++ b/app/ecosystem/[slug]/page.tsx
@@ -0,0 +1,54 @@
+import type { Metadata } from "next";
+import { notFound } from "next/navigation";
+import EcosystemDetail from "@/components/ecosystem/EcosystemDetail";
+import {
+ getAppBySlug,
+ getAppSlugs,
+ renderEcosystemMarkdown,
+} from "@/lib/ecosystem";
+
+type Props = {
+ params: Promise<{ slug: string }>;
+};
+
+export async function generateStaticParams() {
+ return getAppSlugs().map((slug) => ({ slug }));
+}
+
+export async function generateMetadata({ params }: Props): Promise {
+ const { slug } = await params;
+ try {
+ const app = getAppBySlug(slug);
+ return {
+ title: `${app.name} | Livepeer Ecosystem`,
+ description: app.description,
+ openGraph: {
+ title: `${app.name} | Livepeer Ecosystem`,
+ description: app.description,
+ type: "website",
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: `${app.name} | Livepeer Ecosystem`,
+ description: app.description,
+ },
+ };
+ } catch {
+ return { title: "App Not Found — Livepeer Ecosystem" };
+ }
+}
+
+export default async function EcosystemAppPage({ params }: Props) {
+ const { slug } = await params;
+
+ let app;
+ try {
+ app = getAppBySlug(slug);
+ } catch {
+ notFound();
+ }
+
+ const html = await renderEcosystemMarkdown(app.content);
+
+ return ;
+}
diff --git a/app/ecosystem/page.tsx b/app/ecosystem/page.tsx
index b06fc67..9a5dd51 100644
--- a/app/ecosystem/page.tsx
+++ b/app/ecosystem/page.tsx
@@ -1,321 +1,20 @@
-"use client";
-
-import {
- Suspense,
- useState,
- useMemo,
- useEffect,
- useRef,
- useCallback,
-} from "react";
-import { Search, Plus, ArrowUpRight } from "lucide-react";
-import { motion, AnimatePresence } from "framer-motion";
-import { useSearchParams, useRouter, usePathname } from "next/navigation";
-import { ECOSYSTEM_APPS, ECOSYSTEM_CATEGORIES } from "@/lib/ecosystem-data";
-import PageHero from "@/components/ui/PageHero";
-import Container from "@/components/ui/Container";
-import SectionHeader from "@/components/ui/SectionHeader";
-import Badge from "@/components/ui/Badge";
-import Button from "@/components/ui/Button";
-import FilterPill from "@/components/ui/FilterPill";
-
-const BATCH_SIZE = 12;
-
-function EcosystemPageInner() {
- const searchParams = useSearchParams();
- const router = useRouter();
- const pathname = usePathname();
-
- const [activeCategories, setActiveCategories] = useState(() => {
- const param = searchParams.get("categories");
- return param ? param.split(",").map(decodeURIComponent) : [];
- });
- const [search, setSearch] = useState(() => searchParams.get("q") ?? "");
- const [visible, setVisible] = useState(BATCH_SIZE);
- const [buttonBatch, setButtonBatch] = useState(0);
- const sentinelRef = useRef(null);
-
- const isAllActive = activeCategories.length === 0;
- const loadMore = useCallback(() => setVisible((v) => v + BATCH_SIZE), []);
- const handleButtonLoad = useCallback(() => {
- setButtonBatch(visible);
- loadMore();
- }, [visible, loadMore]);
-
- /* Sync filter state → URL query params */
- useEffect(() => {
- const params = new URLSearchParams();
- if (activeCategories.length > 0)
- params.set("categories", activeCategories.join(","));
- if (search) params.set("q", search);
-
- const qs = params.toString();
- const url = qs ? `${pathname}?${qs}` : pathname;
- router.replace(url, { scroll: false });
- }, [activeCategories, search, pathname, router]);
-
- useEffect(() => {
- setVisible(BATCH_SIZE);
- }, [activeCategories, search]);
-
- const handleCategoryToggle = (cat: string) => {
- if (cat === "All") {
- setActiveCategories([]);
- return;
- }
- setActiveCategories((prev) =>
- prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat]
- );
- };
-
- const filtered = useMemo(() => {
- return ECOSYSTEM_APPS.filter((app) => {
- const matchesCategory =
- isAllActive || activeCategories.some((c) => app.categories.includes(c));
- const matchesSearch =
- !search ||
- app.name.toLowerCase().includes(search.toLowerCase()) ||
- app.description.toLowerCase().includes(search.toLowerCase());
- return matchesCategory && matchesSearch;
- });
- }, [activeCategories, search, isAllActive]);
-
- const shown = filtered.slice(0, visible);
- const hasMore = visible < filtered.length;
-
- // Infinite scroll with IntersectionObserver on mobile, "View more" button on desktop.
- useEffect(() => {
- const el = sentinelRef.current;
- if (!el || !hasMore) return;
-
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (entry.isIntersecting) loadMore();
- },
- { rootMargin: "200px" }
- );
- observer.observe(el);
- return () => observer.disconnect();
- }, [hasMore, loadMore]);
-
- return (
-
-
-
-
-
- Submit App
-
- }
- />
-
- {/* Filter bar */}
-
-
- handleCategoryToggle("All")}
- />
- 0}
- onToggle={() => handleCategoryToggle("All")}
- dropdown={{
- items: ECOSYSTEM_CATEGORIES.filter((c) => c !== "All"),
- activeItems: activeCategories,
- onItemToggle: handleCategoryToggle,
- }}
- />
-
-
-
-
-
setSearch(e.target.value)}
- className="w-full rounded-md border border-white/[0.12] bg-white/[0.03] backdrop-blur-sm py-1.5 pl-9 pr-8 text-sm text-white/60 placeholder:text-white/30 transition-colors duration-200 focus:bg-white/[0.05] focus:border-white/20 focus:outline-none sm:w-56 select-none"
- />
-
- {search && (
- setSearch("")}
- className="absolute right-2.5 top-1/2 -translate-y-1/2 cursor-pointer text-white/50 transition-colors hover:text-white/80"
- aria-label="Clear search"
- >
-
-
-
-
- )}
-
-
-
-
- {/* App grid */}
- {shown.length > 0 ? (
-
- {shown.map((app, index) => {
- const inButtonBatch =
- buttonBatch >= 0 &&
- index >= buttonBatch &&
- index < buttonBatch + BATCH_SIZE;
- return (
-
-
-
- {app.logo ? (
-
- ) : (
-
- {app.name.charAt(0)}
-
- )}
-
-
-
-
- {app.name}
-
-
- {app.hostname}
-
-
- {app.description}
-
-
- {app.categories.map((cat) => (
-
- {cat}
-
- ))}
-
-
- );
- })}
-
- ) : (
-
-
- No results found{search ? ` for \u201c${search}\u201d` : ""}
-
-
- Try searching for another term.
- {
- setSearch("");
- setActiveCategories([]);
- }}
- className="cursor-pointer rounded border border-white/10 px-3 py-1 text-xs font-medium text-white/50 transition-colors hover:border-white/20 hover:text-white/80"
- >
- Clear search
-
-
-
-
- )}
-
- {hasMore && (
-
- {/* Infinite scroll on mobile, "View more" button on desktop */}
-
-
- View more
-
-
- )}
-
-
-
- );
-}
+import EcosystemListingClient, {
+ type EcosystemListingApp,
+} from "@/components/ecosystem/EcosystemListingClient";
+import { getAllApps, getEcosystemCategories } from "@/lib/ecosystem";
export default function EcosystemPage() {
- return (
-
-
-
- );
+ const apps: EcosystemListingApp[] = getAllApps().map((app) => ({
+ slug: app.slug,
+ name: app.name,
+ url: app.url,
+ hostname: app.hostname,
+ description: app.description,
+ categories: app.categories,
+ logo: app.logo,
+ logoBg: app.logoBg,
+ }));
+ const categories = getEcosystemCategories();
+
+ return ;
}
diff --git a/app/ecosystem/submit/page.tsx b/app/ecosystem/submit/page.tsx
index 7e15be4..291fce2 100644
--- a/app/ecosystem/submit/page.tsx
+++ b/app/ecosystem/submit/page.tsx
@@ -1,170 +1,7 @@
-"use client";
-
-import { useState } from "react";
-import Link from "next/link";
-import { ECOSYSTEM_CATEGORIES } from "@/lib/ecosystem-data";
-import PageHero from "@/components/ui/PageHero";
-import Container from "@/components/ui/Container";
-import Button from "@/components/ui/Button";
-import FilterPill from "@/components/ui/FilterPill";
-
-const CATEGORIES = ECOSYSTEM_CATEGORIES.filter((c) => c !== "All");
+import SubmitAppForm from "@/components/ecosystem/SubmitAppForm";
+import { getEcosystemCategories } from "@/lib/ecosystem";
export default function SubmitAppPage() {
- const [name, setName] = useState("");
- const [url, setUrl] = useState("");
- const [description, setDescription] = useState("");
- const [categories, setCategories] = useState([]);
- const [email, setEmail] = useState("");
-
- const handleSubmit = (e: React.SyntheticEvent) => {
- e.preventDefault();
-
- const issueUrl = new URL("https://github.com/livepeer/naap/issues/new");
- issueUrl.searchParams.set("template", "ecosystem-submission.yml");
- issueUrl.searchParams.set("title", `Add ${name}`);
- issueUrl.searchParams.set("app-name", name);
- issueUrl.searchParams.set("website", url);
- issueUrl.searchParams.set("description", description);
- issueUrl.searchParams.set("categories", categories.join(", "));
- issueUrl.searchParams.set("contact", email);
-
- window.open(issueUrl.toString(), "_blank");
- };
-
- const isValid = name && url && description && categories.length > 0 && email;
-
- return (
-
-
-
-
- Ecosystem
-
- ›
- Submit
-
-
- Submit your app
-
-
- Built something on Livepeer? Submit your app to be featured in the
- ecosystem directory. This will open a GitHub issue for review.
-
-
-
-
-
- );
+ const categories = getEcosystemCategories().filter((c) => c !== "All");
+ return ;
}
diff --git a/components/ecosystem/EcosystemDetail.tsx b/components/ecosystem/EcosystemDetail.tsx
new file mode 100644
index 0000000..392ffb8
--- /dev/null
+++ b/components/ecosystem/EcosystemDetail.tsx
@@ -0,0 +1,365 @@
+import Link from "next/link";
+import { ArrowUpRight } from "lucide-react";
+import Container from "@/components/ui/Container";
+import Button from "@/components/ui/Button";
+import Badge from "@/components/ui/Badge";
+import PageHero from "@/components/ui/PageHero";
+import type { EcosystemApp } from "@/lib/ecosystem";
+
+const XIcon = ({ className }: { className?: string }) => (
+
+
+
+);
+
+const GitHubIcon = ({ className }: { className?: string }) => (
+
+
+
+);
+
+const MailIcon = ({ className }: { className?: string }) => (
+
+
+
+);
+
+function ConnectButton({
+ href,
+ label,
+ children,
+}: {
+ href: string;
+ label: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+type Props = {
+ app: EcosystemApp;
+ html: string;
+};
+
+const EM_DASH = "—";
+
+function isEmail(value: string): boolean {
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
+}
+
+function handleFromUrl(url: string): string {
+ try {
+ const segments = new URL(url).pathname.split("/").filter(Boolean);
+ return segments[0] ?? url;
+ } catch {
+ return url;
+ }
+}
+
+function ExternalLink({
+ href,
+ children,
+}: {
+ href: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+
+ );
+}
+
+function MetaRow({
+ label,
+ children,
+}: {
+ label: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {label}
+
+
+ {children}
+
+
+ );
+}
+
+function MetaGroup({
+ title,
+ children,
+ isFirst = false,
+}: {
+ title: string;
+ children: React.ReactNode;
+ isFirst?: boolean;
+}) {
+ return (
+
+
+ {title}
+
+
{children}
+
+ );
+}
+
+function LinkOrDash({
+ value,
+ display,
+ showPath = false,
+}: {
+ value: string | undefined;
+ display?: string;
+ showPath?: boolean;
+}) {
+ if (!value) return {EM_DASH} ;
+ if (isEmail(value)) {
+ return (
+
+ {display ?? value}
+
+ );
+ }
+ let host = display;
+ if (!host) {
+ try {
+ const u = new URL(value);
+ const cleanHost = u.hostname.replace(/^www\./, "");
+ const cleanPath = u.pathname.replace(/\/$/, "");
+ // Short-link hosts (Discord invites, Telegram, etc.) are unrecognizable
+ // without their path, so always include it.
+ const isShortLink = /^(discord\.gg|t\.me|bit\.ly|tinyurl\.com)$/i.test(
+ cleanHost
+ );
+ host =
+ showPath || isShortLink ? `${cleanHost}${cleanPath}` : cleanHost;
+ } catch {
+ host = value;
+ }
+ }
+ return {host} ;
+}
+
+export default function EcosystemDetail({ app, html }: Props) {
+ return (
+ <>
+
+
+ {/* Breadcrumb */}
+
+
+ Ecosystem
+
+ /
+ {app.name}
+
+
+ {/* Header */}
+
+
+ {app.logo ? (
+
+ ) : (
+
+ {app.name.charAt(0)}
+
+ )}
+
+
+
+ {app.name}
+
+
+ {app.description}
+
+
+
+ Visit site
+
+
+ {app.twitter && (
+
+
+
+ )}
+ {app.github && (
+
+
+
+ )}
+ {app.contact &&
+ (isEmail(app.contact) ? (
+
+
+
+ ) : (
+
+
+
+ ))}
+
+
+
+
+
+
+ {/* Two-column body lives outside PageHero so the sidebar can use
+ position: sticky — PageHero's overflow-hidden would otherwise
+ break sticky positioning for any descendant. */}
+
+
+
+ {/* Main column */}
+
+
+ {/* Sidebar */}
+
+
+
+
+ {app.madeBy ? (
+ {app.madeBy}
+ ) : (
+ {EM_DASH}
+ )}
+
+
+ {app.categories.length > 0 ? (
+
+ {app.categories.map((cat) => (
+
+ {cat}
+
+ ))}
+
+ ) : (
+ {EM_DASH}
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ← Back to ecosystem
+
+
+
+ >
+ );
+}
diff --git a/components/ecosystem/EcosystemListingClient.tsx b/components/ecosystem/EcosystemListingClient.tsx
new file mode 100644
index 0000000..c823edc
--- /dev/null
+++ b/components/ecosystem/EcosystemListingClient.tsx
@@ -0,0 +1,338 @@
+"use client";
+
+import {
+ Suspense,
+ useState,
+ useMemo,
+ useEffect,
+ useRef,
+ useCallback,
+} from "react";
+import Link from "next/link";
+import { Search, Plus, ArrowUpRight } from "lucide-react";
+import { motion, AnimatePresence } from "framer-motion";
+import { useSearchParams, useRouter, usePathname } from "next/navigation";
+import PageHero from "@/components/ui/PageHero";
+import Container from "@/components/ui/Container";
+import SectionHeader from "@/components/ui/SectionHeader";
+import Badge from "@/components/ui/Badge";
+import Button from "@/components/ui/Button";
+import FilterPill from "@/components/ui/FilterPill";
+
+const BATCH_SIZE = 12;
+
+export type EcosystemListingApp = {
+ slug: string;
+ name: string;
+ url: string;
+ hostname: string;
+ description: string;
+ categories: string[];
+ logo?: string;
+ logoBg?: string;
+};
+
+type Props = {
+ apps: EcosystemListingApp[];
+ categories: string[];
+};
+
+function EcosystemListingInner({ apps, categories }: Props) {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const [activeCategories, setActiveCategories] = useState(() => {
+ const param = searchParams.get("categories");
+ return param ? param.split(",").map(decodeURIComponent) : [];
+ });
+ const [search, setSearch] = useState(() => searchParams.get("q") ?? "");
+ const [visible, setVisible] = useState(BATCH_SIZE);
+ const [buttonBatch, setButtonBatch] = useState(0);
+ const sentinelRef = useRef(null);
+
+ const isAllActive = activeCategories.length === 0;
+ const loadMore = useCallback(() => setVisible((v) => v + BATCH_SIZE), []);
+ const handleButtonLoad = useCallback(() => {
+ setButtonBatch(visible);
+ loadMore();
+ }, [visible, loadMore]);
+
+ /* Sync filter state → URL query params */
+ useEffect(() => {
+ const params = new URLSearchParams();
+ if (activeCategories.length > 0)
+ params.set("categories", activeCategories.join(","));
+ if (search) params.set("q", search);
+
+ const qs = params.toString();
+ const url = qs ? `${pathname}?${qs}` : pathname;
+ router.replace(url, { scroll: false });
+ }, [activeCategories, search, pathname, router]);
+
+ useEffect(() => {
+ setVisible(BATCH_SIZE);
+ }, [activeCategories, search]);
+
+ const handleCategoryToggle = (cat: string) => {
+ if (cat === "All") {
+ setActiveCategories([]);
+ return;
+ }
+ setActiveCategories((prev) =>
+ prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat]
+ );
+ };
+
+ const filtered = useMemo(() => {
+ return apps.filter((app) => {
+ const matchesCategory =
+ isAllActive || activeCategories.some((c) => app.categories.includes(c));
+ const matchesSearch =
+ !search ||
+ app.name.toLowerCase().includes(search.toLowerCase()) ||
+ app.description.toLowerCase().includes(search.toLowerCase());
+ return matchesCategory && matchesSearch;
+ });
+ }, [apps, activeCategories, search, isAllActive]);
+
+ const shown = filtered.slice(0, visible);
+ const hasMore = visible < filtered.length;
+
+ // Infinite scroll with IntersectionObserver on mobile, "View more" button on desktop.
+ useEffect(() => {
+ const el = sentinelRef.current;
+ if (!el || !hasMore) return;
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) loadMore();
+ },
+ { rootMargin: "200px" }
+ );
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, [hasMore, loadMore]);
+
+ return (
+
+
+
+
+
+ Submit App
+
+ }
+ />
+
+ {/* Filter bar */}
+
+
+ handleCategoryToggle("All")}
+ />
+ 0}
+ onToggle={() => handleCategoryToggle("All")}
+ dropdown={{
+ items: categories.filter((c) => c !== "All"),
+ activeItems: activeCategories,
+ onItemToggle: handleCategoryToggle,
+ }}
+ />
+
+
+
+
+
setSearch(e.target.value)}
+ className="w-full rounded-md border border-white/[0.12] bg-white/[0.03] backdrop-blur-sm py-1.5 pl-9 pr-8 text-sm text-white/60 placeholder:text-white/30 transition-colors duration-200 focus:bg-white/[0.05] focus:border-white/20 focus:outline-none sm:w-56 select-none"
+ />
+
+ {search && (
+ setSearch("")}
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 cursor-pointer text-white/50 transition-colors hover:text-white/80"
+ aria-label="Clear search"
+ >
+
+
+
+
+ )}
+
+
+
+
+ {/* App grid */}
+ {shown.length > 0 ? (
+
+ {shown.map((app, index) => {
+ const inButtonBatch =
+ buttonBatch >= 0 &&
+ index >= buttonBatch &&
+ index < buttonBatch + BATCH_SIZE;
+ return (
+
+
+
+
+ {app.logo ? (
+
+ ) : (
+
+ {app.name.charAt(0)}
+
+ )}
+
+
+
+
+ {app.name}
+
+
+ {app.hostname}
+
+
+ {app.description}
+
+
+ {app.categories.map((cat) => (
+
+ {cat}
+
+ ))}
+
+
+
+ );
+ })}
+
+ ) : (
+
+
+ No results found{search ? ` for \u201c${search}\u201d` : ""}
+
+
+ Try searching for another term.
+ {
+ setSearch("");
+ setActiveCategories([]);
+ }}
+ className="cursor-pointer rounded border border-white/10 px-3 py-1 text-xs font-medium text-white/50 transition-colors hover:border-white/20 hover:text-white/80"
+ >
+ Clear search
+
+
+
+
+ )}
+
+ {hasMore && (
+
+ {/* Infinite scroll on mobile, "View more" button on desktop */}
+
+
+ View more
+
+
+ )}
+
+
+
+ );
+}
+
+export default function EcosystemListingClient(props: Props) {
+ return (
+
+
+
+ );
+}
diff --git a/components/ecosystem/SubmitAppForm.tsx b/components/ecosystem/SubmitAppForm.tsx
new file mode 100644
index 0000000..9c8a9e9
--- /dev/null
+++ b/components/ecosystem/SubmitAppForm.tsx
@@ -0,0 +1,171 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import PageHero from "@/components/ui/PageHero";
+import Container from "@/components/ui/Container";
+import Button from "@/components/ui/Button";
+import FilterPill from "@/components/ui/FilterPill";
+
+type Props = {
+ categories: string[];
+};
+
+export default function SubmitAppForm({ categories: allCategories }: Props) {
+ const [name, setName] = useState("");
+ const [url, setUrl] = useState("");
+ const [description, setDescription] = useState("");
+ const [categories, setCategories] = useState([]);
+ const [email, setEmail] = useState("");
+
+ const handleSubmit = (e: React.SyntheticEvent) => {
+ e.preventDefault();
+
+ const issueUrl = new URL("https://github.com/livepeer/naap/issues/new");
+ issueUrl.searchParams.set("template", "ecosystem-submission.yml");
+ issueUrl.searchParams.set("title", `Add ${name}`);
+ issueUrl.searchParams.set("app-name", name);
+ issueUrl.searchParams.set("website", url);
+ issueUrl.searchParams.set("description", description);
+ issueUrl.searchParams.set("categories", categories.join(", "));
+ issueUrl.searchParams.set("contact", email);
+
+ window.open(issueUrl.toString(), "_blank");
+ };
+
+ const isValid = name && url && description && categories.length > 0 && email;
+
+ return (
+
+
+
+
+ Ecosystem
+
+ ›
+ Submit
+
+
+ Submit your app
+
+
+ Built something on Livepeer? Submit your app to be featured in the
+ ecosystem directory. This will open a GitHub issue for review.
+
+
+
+
+
+ App name
+
+ setName(e.target.value)}
+ placeholder="My Livepeer App"
+ className="w-full rounded-lg border border-dark-border bg-dark-card px-4 py-3 text-sm text-white placeholder:text-white/20 focus:border-white/20 focus:outline-none"
+ />
+
+
+
+
+ Website URL
+
+ setUrl(e.target.value)}
+ placeholder="https://myapp.com"
+ className="w-full rounded-lg border border-dark-border bg-dark-card px-4 py-3 text-sm text-white placeholder:text-white/20 focus:border-white/20 focus:outline-none"
+ />
+
+
+
+
+ Description
+
+ setDescription(e.target.value)}
+ placeholder="A short description of what your app does and how it uses Livepeer..."
+ rows={4}
+ className="w-full resize-none rounded-lg border border-dark-border bg-dark-card px-4 py-3 text-sm text-white placeholder:text-white/20 focus:border-white/20 focus:outline-none"
+ />
+
+
+
+
+ Categories
+
+
+ {allCategories.map((cat) => (
+
+ setCategories((prev) =>
+ prev.includes(cat)
+ ? prev.filter((c) => c !== cat)
+ : [...prev, cat]
+ )
+ }
+ />
+ ))}
+
+
+
+
+
+ Contact email
+
+ setEmail(e.target.value)}
+ placeholder="you@example.com"
+ className="w-full rounded-lg border border-dark-border bg-dark-card px-4 py-3 text-sm text-white placeholder:text-white/20 focus:border-white/20 focus:outline-none"
+ />
+
+
+
+ Submit via GitHub
+
+
+
+
+ );
+}
diff --git a/components/home/BuiltOnLivepeer.tsx b/components/home/BuiltOnLivepeer.tsx
index d2b352a..092fce3 100644
--- a/components/home/BuiltOnLivepeer.tsx
+++ b/components/home/BuiltOnLivepeer.tsx
@@ -1,5 +1,6 @@
"use client";
+import Link from "next/link";
import { motion } from "framer-motion";
import Container from "@/components/ui/Container";
import SectionHeader from "@/components/ui/SectionHeader";
@@ -628,13 +629,15 @@ function EmbodyVisual() {
const projects = [
{
+ slug: "daydream",
Visual: DaydreamVisual,
Logo: DaydreamLogo,
description:
- "Turn a live camera feed into AI-generated video, in real time.",
+ "Transform any video into AI-generated visuals in real-time — from a live camera, a clip, or a text prompt.",
domain: "daydream.live",
},
{
+ slug: "frameworks",
Visual: StudioVisual,
Logo: FrameworksLogo,
description:
@@ -642,6 +645,7 @@ const projects = [
domain: "frameworks.network",
},
{
+ slug: "streamplace",
Visual: StreamplaceVisual,
Logo: StreamplaceLogo,
description:
@@ -649,6 +653,7 @@ const projects = [
domain: "stream.place",
},
{
+ slug: "embody",
Visual: EmbodyVisual,
Logo: EmbodyLogo,
description: "Deploy AI avatars that see, speak, and respond in real time.",
@@ -687,41 +692,47 @@ export default function BuiltOnLivepeer() {
transition={{ duration: 0.4 }}
className="lg:row-span-3"
>
-
+
-
+
{featured.description}
{featured.domain}
-
+
{/* 3 smaller cards stacked on the right */}
- {rest.map((project, i) => (
+ {rest.map((project) => (
-
+
-
+
{project.description}
{project.domain}
-
+
))}
@@ -731,7 +742,7 @@ export default function BuiltOnLivepeer() {
transition={{ duration: 0.4 }}
className="mt-12 text-center"
>
-
Explore the ecosystem →
-
+
diff --git a/components/home/StartBuilding.tsx b/components/home/StartBuilding.tsx
index faa43e6..9838d4b 100644
--- a/components/home/StartBuilding.tsx
+++ b/components/home/StartBuilding.tsx
@@ -1,6 +1,7 @@
"use client";
import { useRef, useEffect, useCallback, useState } from "react";
+import Link from "next/link";
import { motion } from "framer-motion";
import Container from "@/components/ui/Container";
import { EXTERNAL_LINKS } from "@/lib/constants";
@@ -290,10 +291,8 @@ export default function StartBuilding() {
generative video, style transfer, and more.
Direct API access to the Livepeer network is coming.{" "}
diff --git a/content/ecosystem/blueclaw.md b/content/ecosystem/blueclaw.md
new file mode 100644
index 0000000..d650125
--- /dev/null
+++ b/content/ecosystem/blueclaw.md
@@ -0,0 +1,44 @@
+---
+name: Blue Claw
+url: https://blueclaw.network
+description: OpenAI-compatible inference for autonomous agents — flat-rate pricing, no rate limits, no token caps.
+categories:
+ - Agents
+ - API
+logo: blueclaw.webp
+order: 5
+madeBy: OpenClaw
+twitter:
+github: https://github.com/blueclaw-network
+contact:
+docs:
+support: https://discord.gg/blueclaw
+terms:
+privacy: https://blueclaw.network/privacy.html
+---
+
+## Overview
+
+Blue Claw is an inference network built specifically for autonomous agents. It exposes an **OpenAI-compatible API** with no rate limits, no token caps, and flat-rate billing — built by agent developers who got tired of unpredictable backpressure from shared inference endpoints.
+
+If your agents need to think continuously without metering you out, Blue Claw is designed to be a drop-in replacement that doesn't punish you for usage.
+
+## What you can build
+
+- **Always-on autonomous agents** — long-running loops that don't get throttled mid-task
+- **Multi-agent systems** — swarms of agents calling inference in parallel without quota juggling
+- **Agent backends with predictable cost** — flat monthly pricing instead of per-token billing
+- **Migrations from OpenAI** — point your existing OpenAI SDK code at Blue Claw with a base URL change
+
+## Developer surface
+
+- **OpenAI-compatible endpoints**
+ - `/v1/chat/completions` — chat
+ - `/v1/embeddings` — text embeddings
+ - `/v1/images/generations` — image generation
+- **Drop-in SDK support** — works with existing Python, JavaScript, and `curl` integrations
+- **Distributed GPU backend** — global redundancy across the inference network
+
+## Pricing
+
+Free during beta. Post-launch: flat-rate $10–$15 per month, no per-token charges.
diff --git a/content/ecosystem/daydream.md b/content/ecosystem/daydream.md
new file mode 100644
index 0000000..9be64ad
--- /dev/null
+++ b/content/ecosystem/daydream.md
@@ -0,0 +1,43 @@
+---
+name: Daydream
+url: https://daydream.live
+description: Open-source, local-first platform for running real-time interactive generative AI video pipelines.
+categories:
+ - AI Video
+ - Generative
+ - API
+logo: daydream.svg
+order: 1
+madeBy: Livepeer Inc
+twitter: https://x.com/DaydreamLiveAI
+github: https://github.com/daydreamlive/scope
+contact: hello@daydream.live
+docs: https://docs.daydream.live/
+support: https://discord.com/invite/mnfGR4Fjhp
+terms: https://daydream.live/terms
+privacy: https://daydream.live/privacy-policy
+---
+
+## Overview
+
+Daydream is an open-source, local-first platform for real-time generative AI video. Its flagship project, **Daydream Scope**, runs autoregressive video diffusion models on your own GPU — or in the cloud — and exposes a node-based workflow for composing live, interactive pipelines from text, video, and live camera inputs.
+
+It's built for developers and creative technologists who want continuous, real-time AI video instead of asynchronous batch generation.
+
+## What you can build
+
+- **Real-time generative video pipelines** — compose text-to-video, video-to-video, and live camera workflows from a node-based graph
+- **Interactive installations and live visuals** — drive output reactively with audio, MIDI, OSC, or DMX
+- **Custom nodes and models** — extend Scope with your own pipelines, LoRAs, and inference logic in Python
+- **Tooling integrations** — share output to TouchDesigner, OBS, Resolume, Unity, Ableton Live, and other creative tools via Spout, Syphon, and NDI
+
+## Developer surface
+
+- **Self-hosted runtime** — Scope runs locally as a server on `localhost:8000` with a WebRTC-based API for programmatic, low-latency control
+- **Bundled diffusion models** — ships with StreamDiffusion V2, LongLive, Krea Realtime, RewardForcing, and MemFlow
+- **Open-source Python codebase** — fork it, build custom nodes, contribute pipelines back upstream
+- **Cloud inference** — run pipelines on remote GPUs when local hardware isn't available
+
+## Powered by Livepeer
+
+Daydream's cloud inference is powered by Livepeer's GPU network. Workloads are routed to independent orchestrators, giving builders elastic, cost-efficient real-time video inference without a centralized provider.
diff --git a/content/ecosystem/embody.md b/content/ecosystem/embody.md
new file mode 100644
index 0000000..5979699
--- /dev/null
+++ b/content/ecosystem/embody.md
@@ -0,0 +1,38 @@
+---
+name: Embody
+url: https://embody.zone
+description: Open-source network for embodied AI avatars — real-time tutoring, telepresence, and on-demand branded content.
+categories:
+ - AI Video
+ - Agents
+logo: embody.svg
+order: 4
+madeBy: DeFine + Dane
+twitter:
+github:
+contact:
+docs:
+support:
+terms:
+privacy:
+---
+
+## Overview
+
+Embody is an open-source network for embodied AI avatars. It provides real-time agent avatars for tutoring, telepresence, and branded content — built so developers can drop expressive, low-latency characters into learning platforms, agent products, and live experiences without building the underlying video pipeline themselves.
+
+## What you can build
+
+- **Real-time AI tutors and tutoring platforms** — embodied agents that see, listen, and respond in live conversations
+- **Telepresence experiences** — agent avatars that represent humans or AI personalities in real time
+- **Branded on-demand avatars** — personalized characters for marketing, customer experience, and education
+- **Custom agent integrations** — drop avatars into your own product instead of building the rendering and inference stack from scratch
+
+## Developer surface
+
+- **Multiple rendering backends** — implementation packages for **Unreal Engine**, **Live2D**, and **Three.js**
+- **Open-source pipeline** — extend or fork the core network for your own avatar workloads
+
+## Powered by Livepeer
+
+Embody runs on Livepeer's GPU infrastructure as a Special Purpose Entity, using the Agent SPE pipeline to generate real-time agent avatars. Workloads are routed across Livepeer orchestrators, giving builders elastic real-time inference for character rendering and agent video without standing up their own GPU fleet.
diff --git a/content/ecosystem/frameworks.md b/content/ecosystem/frameworks.md
new file mode 100644
index 0000000..8cbe165
--- /dev/null
+++ b/content/ecosystem/frameworks.md
@@ -0,0 +1,45 @@
+---
+name: Frameworks
+url: https://frameworks.network
+description: Open streaming stack for live video — full media pipeline from ingest to delivery, self-hosted or cloud.
+categories:
+ - Streaming
+ - Self-hosted
+ - API
+logo: frameworks.svg
+order: 2
+madeBy: DDVTech / MistServer team
+twitter: https://x.com/GetFrames
+github: https://github.com/Livepeer-FrameWorks
+contact: developers@mistserver.org
+docs:
+support:
+terms:
+privacy:
+---
+
+## Overview
+
+Frameworks is an open streaming stack for live video, built on **MistServer** and the **Livepeer** transcoding network. It handles the full media pipeline — ingest, transcoding, processing, storage, and delivery — and ships as a self-hosted platform, a managed "Sovereign Video Cloud," or a hybrid of the two.
+
+It's built for teams that want production-grade live video infrastructure without surrendering ownership of their data or pipeline.
+
+## What you can build
+
+- **Live streaming products** — ingest via RTMP, SRT, or WHIP and deliver to any device or format
+- **VOD workflows** — recordings, clipping, and on-demand playback alongside live
+- **Live AI experiences** — pipe streams through Livepeer's AI inference for real-time transformations
+- **Custom video apps** — embed playback with the Frameworks player packages (React, Svelte, Web Components)
+- **Browser-based production tools** — use StreamCrafter for in-browser stream production
+
+## Developer surface
+
+- **GraphQL and gRPC APIs** — programmatic control over streams, recordings, and accounts
+- **Player SDKs** — drop-in React, Svelte, and Web Component packages
+- **CLI** — manage streams and analytics from the terminal (installable via Homebrew)
+- **Edge + control plane architecture** — edge nodes handle media operations while control and analytics services scale independently
+- **Open source** — the entire monorepo, gitops manifests, and MistServer fork are public on [GitHub](https://github.com/Livepeer-FrameWorks)
+
+## Powered by Livepeer
+
+Frameworks uses Livepeer's decentralized network for transcoding and AI inference, wrapping it in the operational layer that production video applications need. The result is a path to running real, sovereign video infrastructure on top of community-operated GPUs — without rebuilding the pipeline yourself.
diff --git a/content/ecosystem/higher.md b/content/ecosystem/higher.md
new file mode 100644
index 0000000..1baf08c
--- /dev/null
+++ b/content/ecosystem/higher.md
@@ -0,0 +1,30 @@
+---
+name: Higher
+url: https://higher.zip
+description: An onchain creative collective with a manifesto, missions, a shared treasury, and a Farcaster-native experience.
+categories:
+ - Streaming
+ - Community
+ - Decentralized
+logo: higher-zip.svg
+order: 9
+madeBy:
+twitter:
+github:
+contact:
+docs:
+support:
+terms:
+privacy:
+---
+
+## Overview
+
+Higher is an onchain creative collective. It brings together a manifesto, a "Permapool" treasury, ongoing missions, and a shop into a single Farcaster-native experience — a new model for how creators and collectors rally around a shared cultural idea.
+
+## What's inside
+
+- **Manifesto** — the collective's shared philosophy
+- **Permapool** — a permanent shared creative treasury
+- **Missions** — open community quests and contributions
+- **Shop** — collectible drops and merch tied to the collective
diff --git a/content/ecosystem/livepeer-studio.md b/content/ecosystem/livepeer-studio.md
new file mode 100644
index 0000000..d0997ed
--- /dev/null
+++ b/content/ecosystem/livepeer-studio.md
@@ -0,0 +1,42 @@
+---
+name: Livepeer Studio
+url: https://livepeer.studio
+description: Real-time interactive streaming platform — live video, VOD, and transcoding APIs powered by the Livepeer network.
+categories:
+ - Streaming
+ - API
+logo: livepeer-studio.png
+order: 7
+madeBy: Livepeer Inc
+twitter: https://twitter.com/livepeerstudio
+github: https://github.com/livepeer/studio
+contact: https://livepeer.typeform.com/to/HTuUHdDR
+docs: https://docs.livepeer.org/developers/introduction
+support: https://livepeer.statuspage.io/
+terms: https://livepeer.studio/terms-of-service
+privacy: https://livepeer.studio/privacy-policy
+---
+
+## Overview
+
+Livepeer Studio is a real-time interactive streaming platform built on the Livepeer network. It exposes live video, video-on-demand, and transcoding as developer APIs — an open video stack designed for flexibility, with up to 80% cost savings compared to traditional cloud video providers.
+
+It's the fastest path for teams that want to tap Livepeer's network economics without running orchestration infrastructure themselves.
+
+## What you can build
+
+- **Live streaming products** — low-latency live video over WebRTC and HLS
+- **Video-on-demand** — uploads, playback, AI tools, and content integrity features
+- **Custom transcoding pipelines** — scale across modern codecs including AV1
+- **Real-time interactive video** — apps that need sub-second round-trips, not just broadcast
+
+## Developer surface
+
+- **Live Streaming API** — WebRTC and HLS ingest and playback with low-latency delivery
+- **Video On Demand API** — uploads, transcoding, AI tools, and content integrity
+- **Transcode API** — codec coverage including AV1, scales with your traffic
+- **Open source** — the platform itself is on [GitHub](https://github.com/livepeer/studio), and the API is documented for direct integration
+
+## Powered by Livepeer
+
+Every workload routes through the Livepeer network — independent orchestrators contributing GPU and bandwidth — which is what makes Studio's economics and resiliency possible.
diff --git a/content/ecosystem/nytv.md b/content/ecosystem/nytv.md
new file mode 100644
index 0000000..e5fc7f0
--- /dev/null
+++ b/content/ecosystem/nytv.md
@@ -0,0 +1,22 @@
+---
+name: NYTV
+url: https://nytv.live
+description: Independent 24/7 live television station streaming culture and programming from New York.
+categories:
+ - Streaming
+ - Community
+logo: nytv-live.jpg
+order: 10
+madeBy:
+twitter: https://twitter.com/NYTV_
+github:
+contact:
+docs:
+support:
+terms:
+privacy:
+---
+
+## Overview
+
+NYTV (New York Television) is an independent 24/7 live television station streaming culture and programming from New York City. It's a modern take on broadcast television — continuous, live, and built natively for the internet.
diff --git a/content/ecosystem/spritz.md b/content/ecosystem/spritz.md
new file mode 100644
index 0000000..605a8e9
--- /dev/null
+++ b/content/ecosystem/spritz.md
@@ -0,0 +1,37 @@
+---
+name: Spritz
+url: https://app.spritz.chat
+description: Censorship-resistant Web3 chat with HD video calls, livestreaming, and AI agents. Sign in with passkeys or wallets.
+categories:
+ - Streaming
+ - Community
+ - Decentralized
+logo: spritz.svg
+order: 12
+madeBy: Spritz
+twitter:
+github:
+contact:
+docs:
+support:
+terms:
+privacy:
+---
+
+## Overview
+
+Spritz is a censorship-resistant chat app for Web3. Connect with friends using passkeys or wallets, make HD video calls, go live with livestreaming, create AI agents, and chat freely — all on infrastructure that doesn't depend on a central operator.
+
+It's built for communities that want identity, conversations, and live video without surrendering control to a platform.
+
+## What you can do
+
+- **Chat freely** — censorship-resistant peer-to-peer messaging
+- **HD video calls** — real-time video without a central server
+- **Livestreaming** — go live to your community
+- **AI agents** — create and deploy agents inside your conversations
+- **Sign in your way** — passkeys or any supported wallet
+
+## Multi-chain
+
+Spritz works across **Ethereum**, **Base**, and **Solana**, with native apps on web, iOS, and Android. Free to use.
diff --git a/content/ecosystem/streamplace.md b/content/ecosystem/streamplace.md
new file mode 100644
index 0000000..8200f2d
--- /dev/null
+++ b/content/ecosystem/streamplace.md
@@ -0,0 +1,42 @@
+---
+name: Streamplace
+url: https://stream.place
+description: The video layer for decentralized social networks. Open-source, self-hostable, and built on the AT Protocol.
+categories:
+ - Streaming
+ - Decentralized
+logo: stream-place.png
+order: 3
+madeBy: Streamplace
+twitter:
+github: https://github.com/streamplace/streamplace
+contact: eli@stream.place
+docs:
+support:
+terms:
+privacy:
+---
+
+## Overview
+
+Streamplace is the video layer for decentralized social networks. It's a self-hostable, open-source platform that brings high-quality livestreaming, clips, and video uploads to the **AT Protocol** — the same protocol powering Bluesky — and is designed around user sovereignty and content authenticity.
+
+It powers partner platforms like Skylight Social, a TikTok-style app on the AT Protocol.
+
+## What you can build
+
+- **Live video for AT Protocol social apps** — add streaming, clips, and uploads to any Bluesky-compatible client
+- **Sovereign creator tools** — every video is cryptographically signed by its creator and respects consent preferences, built on the same public-key infrastructure as decentralized social
+- **Federated video experiences** — Streamplace nodes connect to any compatible social network to index and serve video content
+- **Self-hosted streaming infrastructure** — run your own node and own your full pipeline
+
+## Developer surface
+
+- **Single-binary node software** — get a Streamplace node running with one command, no complex configuration or deep video expertise required
+- **Open-source monorepo** — hybrid Go + TypeScript codebase, fully public on [GitHub](https://github.com/streamplace/streamplace)
+- **Native client SDKs** — iOS, Android, and web apps for livestreaming, clips, and uploads
+- **AT Protocol integration** — federate with the rest of the AT Protocol ecosystem out of the box
+
+## Powered by Livepeer
+
+Streamplace transcodes every livestream through the Livepeer network for decentralized, low-cost processing and global delivery. The result is open, sovereign video infrastructure that scales on community-operated GPUs instead of a centralized cloud.
diff --git a/content/ecosystem/thelotradio.md b/content/ecosystem/thelotradio.md
new file mode 100644
index 0000000..0529011
--- /dev/null
+++ b/content/ecosystem/thelotradio.md
@@ -0,0 +1,27 @@
+---
+name: The Lot Radio
+url: https://www.thelotradio.com
+description: Independent 24/7 online radio broadcasting live DJ sets from a shipping container in Brooklyn.
+categories:
+ - Streaming
+ - Music
+logo: thelotradio.svg
+logoBg: "#FFFFFF"
+order: 6
+madeBy:
+twitter:
+github:
+contact:
+docs:
+support:
+terms:
+privacy:
+---
+
+## Overview
+
+The Lot Radio is an independent 24/7 online radio station broadcasting from a repurposed shipping container on an empty lot in Brooklyn. It streams a continuous mix of DJ-hosted shows — house, techno, jazz, ambient, experimental electronic, drum & bass, hip-hop, and more — from New York's underground music scene to a global audience.
+
+## Programming
+
+Notable recurring shows include Love Injection, Summer School Radio, and Sorry Records, alongside a rotating schedule of local and international DJs and collectives. Live video and audio run continuously around the clock.
diff --git a/content/ecosystem/tribesocial.md b/content/ecosystem/tribesocial.md
new file mode 100644
index 0000000..c384a09
--- /dev/null
+++ b/content/ecosystem/tribesocial.md
@@ -0,0 +1,45 @@
+---
+name: Tribe Social
+url: https://www.tribesocial.io
+description: Custom branded community apps with courses, live calls, and payments — owned by you, not rented.
+categories:
+ - Streaming
+ - Community
+logo: tribesocial.webp
+order: 8
+madeBy: Tribe Social
+twitter: https://twitter.com/TribeSocialApp
+github:
+contact:
+docs:
+support: https://www.tribesocial.io/help-center
+terms:
+privacy: https://www.tribesocial.io/privacy-policy
+---
+
+## Overview
+
+Tribe Social lets creators and operators launch a custom-branded mobile community app — one they own, not rent. It consolidates community, courses, and live content into a single distraction-free app, and the team helps customers go from zero to launched in 30 days or less.
+
+It's built for community builders who have outgrown generic social platforms and want a branded home for their audience without paying platform fees on every transaction.
+
+## What you can build
+
+- **Branded community apps** — your own iOS and Android apps with your name, logo, and rules
+- **Courses and learning experiences** — exclusive learning modules with audio learning optimized for mobile
+- **Live calls and events** — real-time engagement with replays available on demand
+- **Monetized communities** — integrated payments without platform fees
+
+## Features
+
+- **Community feed** — dynamic interaction space
+- **Courses** — exclusive learning modules
+- **Replays** — on-demand access to recorded calls
+- **Events** — unified calendar
+- **Live calls** — real-time engagement
+- **Audio learning** — mobile-optimized content
+- **Push notifications** — keep members engaged
+
+## Integrations
+
+Zoom, ActiveCampaign, Keap / InfusionSoft, Kajabi, Salesforce, and AddEvent.
diff --git a/content/ecosystem/ufo.md b/content/ecosystem/ufo.md
new file mode 100644
index 0000000..e791f32
--- /dev/null
+++ b/content/ecosystem/ufo.md
@@ -0,0 +1,37 @@
+---
+name: UFO
+url: https://ufo.fm
+description: A home for independent culture — radio, editorial, and weekly mixes from contributors around the world.
+categories:
+ - Streaming
+ - Music
+ - Community
+logo: ufo-fm.svg
+order: 11
+madeBy:
+twitter:
+github:
+contact:
+docs:
+support:
+terms:
+privacy:
+---
+
+## Overview
+
+UFO is a home for independent culture. It publishes radio shows, editorial, and weekly mixes from a network of contributors across London, Copenhagen, Amsterdam, Barcelona, and New York — a distributed alternative to algorithmic audio platforms, with sign-in via Farcaster.
+
+## Programming
+
+A rotating schedule of contributor-led shows, including:
+
+- **Acid Test** — music and conversation on Farcaster
+- **co-tone** — ideas exchanged through music, art, and discussion
+- **FERMI** — underground music from Amsterdam and Bali
+- **Stereo** — exploration of music genres
+- **Sunday 7s** — weekly mixes
+
+## Built on open infrastructure
+
+UFO integrates with a stack of open networks and protocols: **Base**, **Ethereum**, **Farcaster**, **Lens**, **Livepeer**, **Neynar**, **Optimism**, **Paragraph**, **Pods**, **Privy**, **XMTP**, and **Zora**. Livepeer powers the live and on-demand video layer.
diff --git a/data/ecosystem.json b/data/ecosystem.json
deleted file mode 100644
index cb704c7..0000000
--- a/data/ecosystem.json
+++ /dev/null
@@ -1,99 +0,0 @@
-[
- {
- "id": "daydream",
- "name": "Daydream",
- "url": "https://daydream.live",
- "description": "Open-source engine for real-time generative video, audio, and live visuals.",
- "categories": ["AI Video", "Generative", "API"],
- "logo": "daydream.svg"
- },
- {
- "id": "frameworks",
- "name": "Frameworks",
- "url": "https://frameworks.network",
- "description": "Sovereign live streaming platform with SaaS, hybrid, and fully self-hosted modes. No cloud lock-in.",
- "categories": ["Streaming", "Self-hosted", "API"],
- "logo": "frameworks.svg"
- },
- {
- "id": "streamplace",
- "name": "Streamplace",
- "url": "https://stream.place",
- "description": "Streaming video infrastructure for decentralized social networks. Built on the AT Protocol powering Bluesky.",
- "categories": ["Streaming", "Decentralized"],
- "logo": "stream-place.png"
- },
- {
- "id": "embody",
- "name": "Embody",
- "url": "https://embody.zone",
- "description": "Embodied AI avatars for real-time tutoring and telepresence.",
- "categories": ["AI Video", "Agents"],
- "logo": "embody.svg"
- },
- {
- "id": "blueclaw",
- "name": "Blue Claw",
- "url": "https://blueclaw.network",
- "description": "Always-on AI inference for autonomous agents with an OpenAI-compatible API. No rate limits, no throttling.",
- "categories": ["AI Video", "Agents", "API"],
- "logo": "blueclaw.webp"
- },
- {
- "id": "thelotradio",
- "name": "The Lot Radio",
- "url": "https://www.thelotradio.com",
- "description": "Independent 24/7 online radio station broadcasting live DJ sets and music programming from Brooklyn, New York.",
- "categories": ["Streaming", "Music"],
- "logo": "thelotradio.svg",
- "logoBg": "#FFFFFF"
- },
- {
- "id": "livepeer-studio",
- "name": "Livepeer Studio",
- "url": "https://livepeer.studio",
- "description": "Developer APIs for live streaming, video-on-demand, and transcoding powered by the Livepeer network.",
- "categories": ["Streaming", "API"],
- "logo": "livepeer-studio.png"
- },
- {
- "id": "tribesocial",
- "name": "Tribe Social",
- "url": "https://www.tribesocial.io",
- "description": "Branded community apps with live video calls, courses, and integrated payments. Launch in 30 days.",
- "categories": ["Streaming", "Community"],
- "logo": "tribesocial.webp"
- },
- {
- "id": "higher",
- "name": "Higher",
- "url": "https://higher.zip",
- "description": "Onchain community collective on Base with live streaming, missions, and a shared creative treasury.",
- "categories": ["Streaming", "Community", "Decentralized"],
- "logo": "higher-zip.svg"
- },
- {
- "id": "nytv",
- "name": "NYTV",
- "url": "https://nytv.live",
- "description": "Independent 24/7 live television station streaming culture and news programming from New York.",
- "categories": ["Streaming", "Community"],
- "logo": "nytv-live.jpg"
- },
- {
- "id": "ufo",
- "name": "UFO",
- "url": "https://ufo.fm",
- "description": "Broadcast platform for independent culture — radio shows, mixes, and long-form conversations from contributors worldwide.",
- "categories": ["Streaming", "Music", "Community"],
- "logo": "ufo-fm.svg"
- },
- {
- "id": "spritz",
- "name": "Spritz",
- "url": "https://app.spritz.chat",
- "description": "Decentralized social platform combining Web3 messaging, AI agents, livestreaming, and peer-to-peer communication.",
- "categories": ["Streaming", "Community", "Decentralized"],
- "logo": "spritz.svg"
- }
-]
diff --git a/lib/ecosystem-data.ts b/lib/ecosystem-data.ts
deleted file mode 100644
index b38fdbb..0000000
--- a/lib/ecosystem-data.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import ecosystemJson from "@/data/ecosystem.json";
-
-export interface EcosystemApp {
- id: string;
- name: string;
- url: string;
- description: string;
- categories: string[];
- logo?: string;
- logoBg?: string;
-}
-
-export const ECOSYSTEM_CATEGORIES = [
- "All",
- ...Array.from(new Set(ecosystemJson.flatMap((app) => app.categories))).sort(),
-];
-
-export const ECOSYSTEM_APPS: (EcosystemApp & { hostname: string })[] =
- ecosystemJson.map((app) => ({
- ...app,
- hostname: new URL(app.url).hostname.replace("www.", ""),
- }));
diff --git a/lib/ecosystem.ts b/lib/ecosystem.ts
new file mode 100644
index 0000000..97abd9a
--- /dev/null
+++ b/lib/ecosystem.ts
@@ -0,0 +1,110 @@
+import fs from "fs";
+import path from "path";
+import matter from "gray-matter";
+import { unified } from "unified";
+import remarkParse from "remark-parse";
+import remarkRehype from "remark-rehype";
+import rehypeStringify from "rehype-stringify";
+
+const ECOSYSTEM_DIR = path.join(process.cwd(), "content/ecosystem");
+
+export type EcosystemApp = {
+ slug: string;
+ name: string;
+ url: string;
+ hostname: string;
+ description: string;
+ categories: string[];
+ logo?: string;
+ logoBg?: string;
+ madeBy?: string;
+ twitter?: string;
+ github?: string;
+ contact?: string;
+ docs?: string;
+ support?: string;
+ terms?: string;
+ privacy?: string;
+ order?: number;
+ content: string;
+};
+
+function normalize(value: unknown): string | undefined {
+ if (typeof value !== "string") return undefined;
+ const trimmed = value.trim();
+ return trimmed.length > 0 ? trimmed : undefined;
+}
+
+export function getAppSlugs(): string[] {
+ return fs
+ .readdirSync(ECOSYSTEM_DIR)
+ .filter((file) => file.endsWith(".md"))
+ .map((file) => file.replace(/\.md$/, ""));
+}
+
+export function getAppBySlug(slug: string): EcosystemApp {
+ const filePath = path.join(ECOSYSTEM_DIR, `${slug}.md`);
+ const fileContents = fs.readFileSync(filePath, "utf8");
+ const { data, content } = matter(fileContents);
+
+ const url: string = data.url ?? "";
+ let hostname = "";
+ try {
+ hostname = new URL(url).hostname.replace(/^www\./, "");
+ } catch {
+ hostname = url;
+ }
+
+ return {
+ slug,
+ name: data.name ?? slug,
+ url,
+ hostname,
+ description: data.description ?? "",
+ categories: Array.isArray(data.categories) ? data.categories : [],
+ logo: normalize(data.logo),
+ logoBg: normalize(data.logoBg),
+ madeBy: normalize(data.madeBy),
+ twitter: normalize(data.twitter),
+ github: normalize(data.github),
+ contact: normalize(data.contact),
+ docs: normalize(data.docs),
+ support: normalize(data.support),
+ terms: normalize(data.terms),
+ privacy: normalize(data.privacy),
+ order: typeof data.order === "number" ? data.order : undefined,
+ content,
+ };
+}
+
+export function getAllApps(): EcosystemApp[] {
+ const slugs = getAppSlugs();
+ const apps = slugs.map((slug) => getAppBySlug(slug));
+ return apps.sort((a, b) => {
+ const ao = a.order ?? Number.MAX_SAFE_INTEGER;
+ const bo = b.order ?? Number.MAX_SAFE_INTEGER;
+ if (ao !== bo) return ao - bo;
+ return a.name.localeCompare(b.name);
+ });
+}
+
+export function getEcosystemCategories(): string[] {
+ const apps = getAllApps();
+ const cats = new Set();
+ for (const app of apps) {
+ for (const c of app.categories) cats.add(c);
+ }
+ return ["All", ...Array.from(cats).sort()];
+}
+
+export async function renderEcosystemMarkdown(
+ content: string
+): Promise {
+ const result = await unified()
+ .use(remarkParse)
+ .use(remarkRehype, { allowDangerousHtml: true })
+ .use(rehypeStringify, { allowDangerousHtml: true })
+ .process(content);
+
+ return result.toString();
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 615ace2..0b5c72e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,18 +14,12 @@ importers:
gray-matter:
specifier: ^4.0.3
version: 4.0.3
-<<<<<<< HEAD
lucide-react:
specifier: ^1.6.0
version: 1.6.0(react@19.2.4)
next:
specifier: ^15.1.0
version: 15.5.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
-=======
- next:
- specifier: ^15.1.0
- version: 15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
react:
specifier: ^19.0.0
version: 19.2.4
@@ -53,7 +47,6 @@ importers:
devDependencies:
'@eslint/eslintrc':
specifier: ^3
-<<<<<<< HEAD
version: 3.3.5
'@tailwindcss/postcss':
specifier: ^4.0.0
@@ -85,33 +78,6 @@ importers:
tailwindcss:
specifier: ^4.0.0
version: 4.2.2
-=======
- version: 3.3.3
- '@tailwindcss/postcss':
- specifier: ^4.0.0
- version: 4.1.18
- '@types/node':
- specifier: ^22.0.0
- version: 22.19.10
- '@types/react':
- specifier: ^19.0.0
- version: 19.2.13
- '@types/react-dom':
- specifier: ^19.0.0
- version: 19.2.3(@types/react@19.2.13)
- eslint:
- specifier: ^9
- version: 9.39.2(jiti@2.6.1)
- eslint-config-next:
- specifier: ^15.1.0
- version: 15.5.12(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- postcss:
- specifier: ^8.5.0
- version: 8.5.6
- tailwindcss:
- specifier: ^4.0.0
- version: 4.1.18
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
typescript:
specifier: ^5.7.0
version: 5.9.3
@@ -153,21 +119,12 @@ packages:
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-<<<<<<< HEAD
'@eslint/eslintrc@3.3.5':
resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.39.4':
resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==}
-=======
- '@eslint/eslintrc@3.3.3':
- resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
- engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
- '@eslint/js@9.39.2':
- resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.7':
@@ -366,7 +323,6 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
-<<<<<<< HEAD
'@next/env@15.5.14':
resolution: {integrity: sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==}
@@ -375,97 +331,52 @@ packages:
'@next/swc-darwin-arm64@15.5.14':
resolution: {integrity: sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==}
-=======
- '@next/env@15.5.12':
- resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==}
-
- '@next/eslint-plugin-next@15.5.12':
- resolution: {integrity: sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ==}
-
- '@next/swc-darwin-arm64@15.5.12':
- resolution: {integrity: sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
-<<<<<<< HEAD
'@next/swc-darwin-x64@15.5.14':
resolution: {integrity: sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==}
-=======
- '@next/swc-darwin-x64@15.5.12':
- resolution: {integrity: sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
-<<<<<<< HEAD
'@next/swc-linux-arm64-gnu@15.5.14':
resolution: {integrity: sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==}
-=======
- '@next/swc-linux-arm64-gnu@15.5.12':
- resolution: {integrity: sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
-<<<<<<< HEAD
'@next/swc-linux-arm64-musl@15.5.14':
resolution: {integrity: sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==}
-=======
- '@next/swc-linux-arm64-musl@15.5.12':
- resolution: {integrity: sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
-<<<<<<< HEAD
'@next/swc-linux-x64-gnu@15.5.14':
resolution: {integrity: sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==}
-=======
- '@next/swc-linux-x64-gnu@15.5.12':
- resolution: {integrity: sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
-<<<<<<< HEAD
'@next/swc-linux-x64-musl@15.5.14':
resolution: {integrity: sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==}
-=======
- '@next/swc-linux-x64-musl@15.5.12':
- resolution: {integrity: sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
-<<<<<<< HEAD
'@next/swc-win32-arm64-msvc@15.5.14':
resolution: {integrity: sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==}
-=======
- '@next/swc-win32-arm64-msvc@15.5.12':
- resolution: {integrity: sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
-<<<<<<< HEAD
'@next/swc-win32-x64-msvc@15.5.14':
resolution: {integrity: sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==}
-=======
- '@next/swc-win32-x64-msvc@15.5.12':
- resolution: {integrity: sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -495,7 +406,6 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
-<<<<<<< HEAD
'@tailwindcss/node@4.2.2':
resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
@@ -532,94 +442,33 @@ packages:
'@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
engines: {node: '>= 20'}
-=======
- '@tailwindcss/node@4.1.18':
- resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
-
- '@tailwindcss/oxide-android-arm64@4.1.18':
- resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [android]
-
- '@tailwindcss/oxide-darwin-arm64@4.1.18':
- resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [darwin]
-
- '@tailwindcss/oxide-darwin-x64@4.1.18':
- resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [darwin]
-
- '@tailwindcss/oxide-freebsd-x64@4.1.18':
- resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [freebsd]
-
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
- resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
- engines: {node: '>= 10'}
- cpu: [arm]
- os: [linux]
-
- '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
- resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
- engines: {node: '>= 10'}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
cpu: [arm64]
os: [linux]
libc: [glibc]
-<<<<<<< HEAD
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'}
-=======
- '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
- resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
- engines: {node: '>= 10'}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
cpu: [arm64]
os: [linux]
libc: [musl]
-<<<<<<< HEAD
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'}
-=======
- '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
- resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
- engines: {node: '>= 10'}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
cpu: [x64]
os: [linux]
libc: [glibc]
-<<<<<<< HEAD
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'}
-=======
- '@tailwindcss/oxide-linux-x64-musl@4.1.18':
- resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
- engines: {node: '>= 10'}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
cpu: [x64]
os: [linux]
libc: [musl]
-<<<<<<< HEAD
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
-=======
- '@tailwindcss/oxide-wasm32-wasi@4.1.18':
- resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
@@ -630,7 +479,6 @@ packages:
- '@emnapi/wasi-threads'
- tslib
-<<<<<<< HEAD
'@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==}
engines: {node: '>= 20'}
@@ -649,26 +497,6 @@ packages:
'@tailwindcss/postcss@4.2.2':
resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==}
-=======
- '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
- resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [win32]
-
- '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
- resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [win32]
-
- '@tailwindcss/oxide@4.1.18':
- resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
- engines: {node: '>= 10'}
-
- '@tailwindcss/postcss@4.1.18':
- resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -694,26 +522,16 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
-<<<<<<< HEAD
'@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
-=======
- '@types/node@22.19.10':
- resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
'@types/react': ^19.2.0
-<<<<<<< HEAD
'@types/react@19.2.14':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
-=======
- '@types/react@19.2.13':
- resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@@ -1148,13 +966,8 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
-<<<<<<< HEAD
eslint-config-next@15.5.14:
resolution: {integrity: sha512-lmJ5F8ZgOYogq0qtH4L5SpxuASY2SPdOzqUprN2/56+P3GPsIpXaUWIJC66kYIH+yZdsM4nkHE5MIBP6s1NiBw==}
-=======
- eslint-config-next@15.5.12:
- resolution: {integrity: sha512-ktW3XLfd+ztEltY5scJNjxjHwtKWk6vU2iwzZqSN09UsbBmMeE/cVlJ1yESg6Yx5LW7p/Z8WzUAgYXGLEmGIpg==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
peerDependencies:
eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
typescript: '>=3.3.1'
@@ -1249,13 +1062,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
-<<<<<<< HEAD
eslint@9.39.4:
resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==}
-=======
- eslint@9.39.2:
- resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
@@ -1651,138 +1459,78 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
-<<<<<<< HEAD
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
-=======
- lightningcss-android-arm64@1.30.2:
- resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
-<<<<<<< HEAD
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
-=======
- lightningcss-darwin-arm64@1.30.2:
- resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
-<<<<<<< HEAD
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
-=======
- lightningcss-darwin-x64@1.30.2:
- resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
-<<<<<<< HEAD
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
-=======
- lightningcss-freebsd-x64@1.30.2:
- resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
-<<<<<<< HEAD
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
-=======
- lightningcss-linux-arm-gnueabihf@1.30.2:
- resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
-<<<<<<< HEAD
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
-=======
- lightningcss-linux-arm64-gnu@1.30.2:
- resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
-<<<<<<< HEAD
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
-=======
- lightningcss-linux-arm64-musl@1.30.2:
- resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
-<<<<<<< HEAD
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
-=======
- lightningcss-linux-x64-gnu@1.30.2:
- resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
-<<<<<<< HEAD
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
-=======
- lightningcss-linux-x64-musl@1.30.2:
- resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
-<<<<<<< HEAD
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
-=======
- lightningcss-win32-arm64-msvc@1.30.2:
- resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
-<<<<<<< HEAD
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
-=======
- lightningcss-win32-x64-msvc@1.30.2:
- resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
-<<<<<<< HEAD
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
-=======
- lightningcss@1.30.2:
- resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: '>= 12.0.0'}
locate-path@6.0.0:
@@ -1799,14 +1547,11 @@ packages:
lowlight@3.3.0:
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
-<<<<<<< HEAD
lucide-react@1.6.0:
resolution: {integrity: sha512-YxLKVCOF5ZDI1AhKQE5IBYMY9y/Nr4NT15+7QEWpsTSVCdn4vmZhww+6BP76jWYjQx8rSz1Z+gGme1f+UycWEw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
-=======
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1926,13 +1671,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
-<<<<<<< HEAD
next@15.5.14:
resolution: {integrity: sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==}
-=======
- next@15.5.12:
- resolution: {integrity: sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -2038,13 +1778,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
-<<<<<<< HEAD
postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
-=======
- postcss@8.5.6:
- resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
@@ -2272,13 +2007,8 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
-<<<<<<< HEAD
tailwindcss@4.2.2:
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
-=======
- tailwindcss@4.1.18:
- resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
tapable@2.3.0:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
@@ -2427,15 +2157,9 @@ snapshots:
tslib: 2.8.1
optional: true
-<<<<<<< HEAD
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))':
dependencies:
eslint: 9.39.4(jiti@2.6.1)
-=======
- '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))':
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@@ -2456,11 +2180,7 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
-<<<<<<< HEAD
'@eslint/eslintrc@3.3.5':
-=======
- '@eslint/eslintrc@3.3.3':
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
dependencies:
ajv: 6.14.0
debug: 4.4.3
@@ -2474,11 +2194,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
-<<<<<<< HEAD
'@eslint/js@9.39.4': {}
-=======
- '@eslint/js@9.39.2': {}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
'@eslint/object-schema@2.1.7': {}
@@ -2621,7 +2337,6 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
-<<<<<<< HEAD
'@next/env@15.5.14': {}
'@next/eslint-plugin-next@15.5.14':
@@ -2650,36 +2365,6 @@ snapshots:
optional: true
'@next/swc-win32-x64-msvc@15.5.14':
-=======
- '@next/env@15.5.12': {}
-
- '@next/eslint-plugin-next@15.5.12':
- dependencies:
- fast-glob: 3.3.1
-
- '@next/swc-darwin-arm64@15.5.12':
- optional: true
-
- '@next/swc-darwin-x64@15.5.12':
- optional: true
-
- '@next/swc-linux-arm64-gnu@15.5.12':
- optional: true
-
- '@next/swc-linux-arm64-musl@15.5.12':
- optional: true
-
- '@next/swc-linux-x64-gnu@15.5.12':
- optional: true
-
- '@next/swc-linux-x64-musl@15.5.12':
- optional: true
-
- '@next/swc-win32-arm64-msvc@15.5.12':
- optional: true
-
- '@next/swc-win32-x64-msvc@15.5.12':
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -2704,16 +2389,11 @@ snapshots:
dependencies:
tslib: 2.8.1
-<<<<<<< HEAD
'@tailwindcss/node@4.2.2':
-=======
- '@tailwindcss/node@4.1.18':
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.20.1
jiti: 2.6.1
-<<<<<<< HEAD
lightningcss: 1.32.0
magic-string: 0.30.21
source-map-js: 1.2.1
@@ -2777,71 +2457,6 @@ snapshots:
'@tailwindcss/oxide': 4.2.2
postcss: 8.5.8
tailwindcss: 4.2.2
-=======
- lightningcss: 1.30.2
- magic-string: 0.30.21
- source-map-js: 1.2.1
- tailwindcss: 4.1.18
-
- '@tailwindcss/oxide-android-arm64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-darwin-arm64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-darwin-x64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-freebsd-x64@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-linux-x64-musl@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-wasm32-wasi@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
- optional: true
-
- '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
- optional: true
-
- '@tailwindcss/oxide@4.1.18':
- optionalDependencies:
- '@tailwindcss/oxide-android-arm64': 4.1.18
- '@tailwindcss/oxide-darwin-arm64': 4.1.18
- '@tailwindcss/oxide-darwin-x64': 4.1.18
- '@tailwindcss/oxide-freebsd-x64': 4.1.18
- '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
- '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
- '@tailwindcss/oxide-linux-arm64-musl': 4.1.18
- '@tailwindcss/oxide-linux-x64-gnu': 4.1.18
- '@tailwindcss/oxide-linux-x64-musl': 4.1.18
- '@tailwindcss/oxide-wasm32-wasi': 4.1.18
- '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
- '@tailwindcss/oxide-win32-x64-msvc': 4.1.18
-
- '@tailwindcss/postcss@4.1.18':
- dependencies:
- '@alloc/quick-lru': 5.2.0
- '@tailwindcss/node': 4.1.18
- '@tailwindcss/oxide': 4.1.18
- postcss: 8.5.6
- tailwindcss: 4.1.18
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
'@tybys/wasm-util@0.10.1':
dependencies:
@@ -2868,7 +2483,6 @@ snapshots:
'@types/ms@2.1.0': {}
-<<<<<<< HEAD
'@types/node@22.19.15':
dependencies:
undici-types: 6.21.0
@@ -2878,23 +2492,11 @@ snapshots:
'@types/react': 19.2.14
'@types/react@19.2.14':
-=======
- '@types/node@22.19.10':
- dependencies:
- undici-types: 6.21.0
-
- '@types/react-dom@19.2.3(@types/react@19.2.13)':
- dependencies:
- '@types/react': 19.2.13
-
- '@types/react@19.2.13':
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
dependencies:
csstype: 3.2.3
'@types/unist@3.0.3': {}
-<<<<<<< HEAD
'@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -2904,17 +2506,6 @@ snapshots:
'@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.57.1
eslint: 9.39.4(jiti@2.6.1)
-=======
- '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.57.1
- '@typescript-eslint/type-utils': 8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/utils': 8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.57.1
- eslint: 9.39.2(jiti@2.6.1)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -2922,22 +2513,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
-<<<<<<< HEAD
'@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
-=======
- '@typescript-eslint/parser@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
dependencies:
'@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.57.1
debug: 4.4.3
-<<<<<<< HEAD
eslint: 9.39.4(jiti@2.6.1)
-=======
- eslint: 9.39.2(jiti@2.6.1)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -2960,7 +2543,6 @@ snapshots:
dependencies:
typescript: 5.9.3
-<<<<<<< HEAD
'@typescript-eslint/type-utils@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.57.1
@@ -2968,15 +2550,6 @@ snapshots:
'@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.4(jiti@2.6.1)
-=======
- '@typescript-eslint/type-utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@typescript-eslint/types': 8.57.1
- '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
- '@typescript-eslint/utils': 8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- debug: 4.4.3
- eslint: 9.39.2(jiti@2.6.1)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
@@ -2999,7 +2572,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
-<<<<<<< HEAD
'@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
@@ -3007,15 +2579,6 @@ snapshots:
'@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
eslint: 9.39.4(jiti@2.6.1)
-=======
- '@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
- '@typescript-eslint/scope-manager': 8.57.1
- '@typescript-eslint/types': 8.57.1
- '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -3438,7 +3001,6 @@ snapshots:
escape-string-regexp@4.0.0: {}
-<<<<<<< HEAD
eslint-config-next@15.5.14(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 15.5.14
@@ -3452,21 +3014,6 @@ snapshots:
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@2.6.1))
-=======
- eslint-config-next@15.5.12(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
- dependencies:
- '@next/eslint-plugin-next': 15.5.12
- '@rushstack/eslint-patch': 1.16.1
- '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/parser': 8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
- eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1))
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
@@ -3486,26 +3033,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
-<<<<<<< HEAD
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
eslint: 9.39.4(jiti@2.6.1)
-=======
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- '@nolyfill/is-core-module': 1.0.39
- debug: 4.4.3
- eslint: 9.39.2(jiti@2.6.1)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
get-tsconfig: 4.13.7
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
-<<<<<<< HEAD
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -3522,24 +3060,6 @@ snapshots:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
-=======
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
- transitivePeerDependencies:
- - supports-color
-
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- debug: 3.2.7
- optionalDependencies:
- '@typescript-eslint/parser': 8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.2(jiti@2.6.1)
- eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
- transitivePeerDependencies:
- - supports-color
-
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -3548,15 +3068,9 @@ snapshots:
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
-<<<<<<< HEAD
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
-=======
- eslint: 9.39.2(jiti@2.6.1)
- eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -3568,21 +3082,13 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
-<<<<<<< HEAD
'@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
-=======
- '@typescript-eslint/parser': 8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
-<<<<<<< HEAD
eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)):
-=======
- eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)):
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
dependencies:
aria-query: 5.3.2
array-includes: 3.1.9
@@ -3592,11 +3098,7 @@ snapshots:
axobject-query: 4.1.0
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
-<<<<<<< HEAD
eslint: 9.39.4(jiti@2.6.1)
-=======
- eslint: 9.39.2(jiti@2.6.1)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
hasown: 2.0.2
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
@@ -3605,19 +3107,11 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
-<<<<<<< HEAD
eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)):
dependencies:
eslint: 9.39.4(jiti@2.6.1)
eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.6.1)):
-=======
- eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)):
- dependencies:
- eslint: 9.39.2(jiti@2.6.1)
-
- eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)):
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
dependencies:
array-includes: 3.1.9
array.prototype.findlast: 1.2.5
@@ -3625,11 +3119,7 @@ snapshots:
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.3.1
-<<<<<<< HEAD
eslint: 9.39.4(jiti@2.6.1)
-=======
- eslint: 9.39.2(jiti@2.6.1)
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
@@ -3654,26 +3144,15 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
-<<<<<<< HEAD
eslint@9.39.4(jiti@2.6.1):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
-=======
- eslint@9.39.2(jiti@2.6.1):
- dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.2
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
-<<<<<<< HEAD
'@eslint/eslintrc': 3.3.5
'@eslint/js': 9.39.4
-=======
- '@eslint/eslintrc': 3.3.3
- '@eslint/js': 9.39.2
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
@@ -4104,7 +3583,6 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
-<<<<<<< HEAD
lightningcss-android-arm64@1.32.0:
optional: true
@@ -4153,56 +3631,6 @@ snapshots:
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
-=======
- lightningcss-android-arm64@1.30.2:
- optional: true
-
- lightningcss-darwin-arm64@1.30.2:
- optional: true
-
- lightningcss-darwin-x64@1.30.2:
- optional: true
-
- lightningcss-freebsd-x64@1.30.2:
- optional: true
-
- lightningcss-linux-arm-gnueabihf@1.30.2:
- optional: true
-
- lightningcss-linux-arm64-gnu@1.30.2:
- optional: true
-
- lightningcss-linux-arm64-musl@1.30.2:
- optional: true
-
- lightningcss-linux-x64-gnu@1.30.2:
- optional: true
-
- lightningcss-linux-x64-musl@1.30.2:
- optional: true
-
- lightningcss-win32-arm64-msvc@1.30.2:
- optional: true
-
- lightningcss-win32-x64-msvc@1.30.2:
- optional: true
-
- lightningcss@1.30.2:
- dependencies:
- detect-libc: 2.1.2
- optionalDependencies:
- lightningcss-android-arm64: 1.30.2
- lightningcss-darwin-arm64: 1.30.2
- lightningcss-darwin-x64: 1.30.2
- lightningcss-freebsd-x64: 1.30.2
- lightningcss-linux-arm-gnueabihf: 1.30.2
- lightningcss-linux-arm64-gnu: 1.30.2
- lightningcss-linux-arm64-musl: 1.30.2
- lightningcss-linux-x64-gnu: 1.30.2
- lightningcss-linux-x64-musl: 1.30.2
- lightningcss-win32-arm64-msvc: 1.30.2
- lightningcss-win32-x64-msvc: 1.30.2
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
locate-path@6.0.0:
dependencies:
@@ -4220,13 +3648,10 @@ snapshots:
devlop: 1.1.0
highlight.js: 11.11.1
-<<<<<<< HEAD
lucide-react@1.6.0(react@19.2.4):
dependencies:
react: 19.2.4
-=======
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -4430,15 +3855,9 @@ snapshots:
natural-compare@1.4.0: {}
-<<<<<<< HEAD
next@15.5.14(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 15.5.14
-=======
- next@15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
- dependencies:
- '@next/env': 15.5.12
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001781
postcss: 8.4.31
@@ -4446,7 +3865,6 @@ snapshots:
react-dom: 19.2.4(react@19.2.4)
styled-jsx: 5.1.6(react@19.2.4)
optionalDependencies:
-<<<<<<< HEAD
'@next/swc-darwin-arm64': 15.5.14
'@next/swc-darwin-x64': 15.5.14
'@next/swc-linux-arm64-gnu': 15.5.14
@@ -4455,16 +3873,6 @@ snapshots:
'@next/swc-linux-x64-musl': 15.5.14
'@next/swc-win32-arm64-msvc': 15.5.14
'@next/swc-win32-x64-msvc': 15.5.14
-=======
- '@next/swc-darwin-arm64': 15.5.12
- '@next/swc-darwin-x64': 15.5.12
- '@next/swc-linux-arm64-gnu': 15.5.12
- '@next/swc-linux-arm64-musl': 15.5.12
- '@next/swc-linux-x64-gnu': 15.5.12
- '@next/swc-linux-x64-musl': 15.5.12
- '@next/swc-win32-arm64-msvc': 15.5.12
- '@next/swc-win32-x64-msvc': 15.5.12
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -4566,11 +3974,7 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
-<<<<<<< HEAD
postcss@8.5.8:
-=======
- postcss@8.5.6:
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -4882,11 +4286,7 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
-<<<<<<< HEAD
tailwindcss@4.2.2: {}
-=======
- tailwindcss@4.1.18: {}
->>>>>>> 6c5ab2a (Add dev container and migrate to pnpm for supply chain security)
tapable@2.3.0: {}
diff --git a/scripts/check-ecosystem-urls.mjs b/scripts/check-ecosystem-urls.mjs
index e3c25d7..4c380ae 100644
--- a/scripts/check-ecosystem-urls.mjs
+++ b/scripts/check-ecosystem-urls.mjs
@@ -3,13 +3,35 @@
* Exits with code 1 if any URL is broken.
*/
-import { readFileSync } from "node:fs";
-import { resolve, dirname } from "node:path";
+import { readFileSync, readdirSync } from "node:fs";
+import { resolve, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
-const dataPath = resolve(__dirname, "../data/ecosystem.json");
-const apps = JSON.parse(readFileSync(dataPath, "utf-8"));
+const ecosystemDir = resolve(__dirname, "../content/ecosystem");
+
+/**
+ * Minimal frontmatter parser — extracts `name` and `url` from each markdown
+ * file. Avoids pulling in gray-matter for a one-file CI script.
+ */
+function parseFrontmatter(source) {
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
+ if (!match) return {};
+ const data = {};
+ for (const line of match[1].split(/\r?\n/)) {
+ const m = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
+ if (!m) continue;
+ const value = m[2].trim();
+ if (value === "") continue;
+ data[m[1]] = value.replace(/^["']|["']$/g, "");
+ }
+ return data;
+}
+
+const apps = readdirSync(ecosystemDir)
+ .filter((f) => f.endsWith(".md"))
+ .map((f) => parseFrontmatter(readFileSync(join(ecosystemDir, f), "utf-8")))
+ .filter((app) => app.url);
const TIMEOUT_MS = 15_000;
const MAX_RETRIES = 2;