Skip to content

feat: docs-v2 #986

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 24 commits into
base: develop
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
7 changes: 6 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"react-hooks/rules-of-hooks": "off",
"@next/next/no-img-element": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-unused-vars": "off",
"react-hooks/exhaustive-deps": "off"
}
}
16 changes: 0 additions & 16 deletions .github/PULL_REQUEST_TEMPLATE.md

This file was deleted.

42 changes: 0 additions & 42 deletions .github/mlc_config.json

This file was deleted.

16 changes: 0 additions & 16 deletions .github/workflows/board-automation.yaml

This file was deleted.

14 changes: 0 additions & 14 deletions .github/workflows/linkcheck-prs.yml

This file was deleted.

27 changes: 27 additions & 0 deletions .github/workflows/linkcheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: linkcheck

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:

jobs:
lint:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run link validation
run: bun run lint
26 changes: 0 additions & 26 deletions .github/workflows/stylecheck-prs.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -8,4 +8,5 @@ openapi
.cache
.cursorrules
.next
.source/
tmp
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Hiro Docs
210 changes: 101 additions & 109 deletions app/(docs)/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,135 +1,127 @@
import type { Metadata } from "next";
import { Card, Cards } from "fumadocs-ui/components/card";
import { RollButton } from "fumadocs-ui/components/roll-button";
import { DocsPage, DocsBody } from "fumadocs-ui/page";
import fs from "fs/promises";
import matter from "gray-matter";
import { source } from "@/lib/source";
import { openapi } from "@/lib/source";
import { notFound } from "next/navigation";
import { utils, type Page } from "@/utils/source";
import { createMetadata, getRouteMetadata } from "@/utils/metadata";

interface Param {
slug: string[];
}
import { HeadingProps } from "@/types";
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/components/ui/accordion";
import {
DocsPage,
DocsBody,
DocsDescription,
DocsTitle,
} from "@/components/page";
import { CustomTable as Table, TableProps } from "@/components/table";
import { OrderedList, UnorderedList } from "@/components/lists";
import { Callout } from "@/components/callout";
import { Cards, Card, SecondaryCard } from "@/components/card";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { docskit } from "@/components/docskit/components";
import defaultMdxComponents from "fumadocs-ui/mdx";
import { LLMShare } from "@/components/llm-share";

export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();

export const dynamicParams = false;
const fileContent = await fs.readFile(page.data._file.absolutePath, "utf-8");
const { content: rawMarkdownContent } = matter(fileContent);

export default function Page({ params }: { params: Param }): JSX.Element {
const page = utils.getPage(params.slug);
const LLMContent = rawMarkdownContent
.split("\n")
.filter((line) => !line.trim().startsWith("import"))
.join("\n");

if (!page) notFound();
const MDX = page.data.body;

return (
<DocsPage
toc={page.data.exports.toc}
toc={page.data.toc}
full={page.data.full}
tableOfContent={{
enabled: page.data.toc,
style: "clerk",
}}
>
<RollButton />
{page.data.title !== "Home" && (
<h1 className="text-2xl text-foreground sm:text-3xl">
{page.data.title}
</h1>
)}
{page.data.title !== "Home" && (
<p className="mb-8 text-lg text-muted-foreground">
{page.data.description}
</p>
)}
{page.data.title !== "Home" && (
<hr className="border-t border-border/50" />
)}
<div className="mb-4">
<div className="flex justify-between items-start gap-4">
{(!params.slug?.includes("stacks") || params.slug?.length > 1) &&
(!params.slug?.includes("bitcoin") || params.slug?.length > 1) && (
<DocsTitle className="mt-0">{page.data.title}</DocsTitle>
)}
{page.data.llm && <LLMShare content={LLMContent} />}
</div>
<DocsDescription>{page.data.description}</DocsDescription>
</div>

<DocsBody>
{page.data.index ? (
<Category page={page} />
) : (
<page.data.exports.default />
)}
<MDX
components={{
...defaultMdxComponents,
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
APIPage: openapi.APIPage,
h1: ({ children, ...props }: HeadingProps) => {
const H1 =
defaultMdxComponents.h1 as React.ComponentType<HeadingProps>;
const id = typeof children === "string" ? children : undefined;
return (
<H1 id={id} {...props}>
{children}
</H1>
);
},
blockquote: (props) => <Callout>{props.children}</Callout>,
Callout,
Cards,
Card,
SecondaryCard,
code: (props: React.PropsWithChildren) => (
<code
{...props}
className={`border border-border rounded-md p-1 bg-code text-sm text-muted-foreground [h1_&]:text-xl`}
/>
),
hr: (props: React.PropsWithChildren) => (
<hr {...props} className="border-t border-border/50 mt-0 mb-6" />
),
table: (props: TableProps) => <Table {...props} />,
ol: OrderedList,
ul: UnorderedList,
Tabs,
TabsList,
TabsTrigger,
TabsContent,
...docskit,
}}
/>
</DocsBody>
</DocsPage>
);
}

function Category({ page }: { page: Page }): JSX.Element {
const filtered = utils.files.filter(
(docs) =>
docs.type === "page" &&
docs.file.dirname === page.file.dirname &&
docs.file.name !== "index"
) as Page[];

return (
<Cards>
{filtered.map((item) => (
<Card
key={item.url}
title={item.data.title}
description={item.data.description ?? "No Description"}
href={item.url}
/>
))}
</Cards>
);
export async function generateStaticParams() {
return source.generateParams();
}

export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>;
}): Promise<Metadata> {
}) {
const params = await props.params;
const page = utils.getPage(params.slug);
const page = source.getPage(params.slug);
if (!page) notFound();

const path = `/${params.slug?.join("/") || ""}`;
const routeMetadata = getRouteMetadata(path);

const pathParts = path.split("/").filter(Boolean);

const genericTitles = [
"Overview",
"Installation",
"Quickstart",
"Concepts",
"Getting Started",
];

let title = page.data.title;

if (page.file.name === "index" || genericTitles.includes(title)) {
let sectionName =
page.file.name === "index"
? pathParts[pathParts.length - 1]
: pathParts[pathParts.length - 2] || pathParts[pathParts.length - 1];

if (sectionName === "api" && pathParts.length >= 2) {
const parentSection = pathParts[pathParts.length - 2];
if (parentSection === "runes" || parentSection === "ordinals") {
const capitalizedParent =
parentSection.charAt(0).toUpperCase() + parentSection.slice(1);
sectionName = `${capitalizedParent} API`;
}
}

const sectionTitle = sectionName
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
.replace("Api", "API")
.replace("Js", "JS")
.replace("Sdk", "SDK");

if (page.file.name === "index") {
title = `${sectionTitle} Overview`;
} else {
title = `${sectionTitle} ${title}`;
}
}

const pageMetadata: Partial<Metadata> = {
title,
return {
title: page.data.title,
description: page.data.description,
};

return createMetadata({
...routeMetadata,
...pageMetadata,
});
}
110 changes: 0 additions & 110 deletions app/(docs)/layout.client.tsx

This file was deleted.

103 changes: 20 additions & 83 deletions app/(docs)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,88 +1,25 @@
import { DocsLayout, type DocsLayoutProps } from "fumadocs-ui/layout";
import { DocsLayout } from "@/components/docs";
import type { ReactNode } from "react";
import { ArrowUpRight } from "lucide-react";
import { utils } from "@/utils/source";
import { DocsLogo } from "@/components/ui/icon";
import { Body, NavChildren, SidebarBanner } from "./layout.client";
import { Statuspage } from "statuspage.io";
import { baseOptions } from "@/app/layout.config";
import { source } from "@/lib/source";
import { TopNav } from "@/components/top-nav";

const statuspage = new Statuspage("3111l89394q4");
// console.log({ status: await statuspage.api.getStatus() });

export const layoutOptions: Omit<DocsLayoutProps, "children"> = {
tree: utils.pageTree,
nav: {
transparentMode: "top",
title: <DocsLogo className="size-28 hidden sm:block" />,
children: <NavChildren />,
links: [
{
label: "Hiro Platform",
href: "https://platform.hiro.so/",
icon: (
<div className="flex items-center gap-1 bg-secondary p-1.5 rounded-md">
<span className="font-semibold max-md:hidden">Hiro Platform</span>
</div>
),
external: true,
},
],
},
links: [
{
text: "Guides",
url: "/guides",
},
{
text: "Cookbook",
url: "/cookbook",
},
],
sidebar: {
defaultOpenLevel: 0,
banner: <SidebarBanner />,
},
};

export const homeLayoutOptions: Omit<DocsLayoutProps, "children"> = {
tree: utils.pageTree,
nav: {
transparentMode: "top",
title: <DocsLogo className="size-28 hidden sm:block" />,
children: null,
links: [
{
label: "Hiro Platform",
href: "https://platform.hiro.so/",
icon: (
<div className="flex items-center gap-1 bg-secondary p-1.5 rounded-md">
<span className="font-semibold max-md:hidden">Hiro Platform</span>
</div>
),
external: true,
},
],
},
links: [
{
text: "Guides",
url: "/guides",
},
{
text: "Cookbook",
url: "/cookbook",
},
],
};

export default function Layout({
children,
}: {
children: ReactNode;
}): JSX.Element {
export default function Layout({ children }: { children: ReactNode }) {
return (
<Body>
<DocsLayout {...layoutOptions}>{children}</DocsLayout>
</Body>
<DocsLayout
{...baseOptions}
tree={source.pageTree}
sidebar={{
tabs: false,
hideSearch: true,
collapsible: false,
}}
// tabMode="navbar" // Set tabMode to navbar to display tabs in the navigation bar
nav={{
component: <TopNav />,
}}
>
{children}
</DocsLayout>
);
}
36 changes: 36 additions & 0 deletions app/(docs)/raw/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as fs from "node:fs/promises";
import path from "node:path";
import { NextRequest } from "next/server";

// Base directory for documentation content (same as in the llms API route)
const DOCS_CONTENT_PATH = path.join(process.cwd(), "content/docs");

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
const { slug } = await params;

if (slug.length === 0) {
return new Response("Not Found", { status: 404 });
}

const filePath = path.join(DOCS_CONTENT_PATH, ...slug);

try {
await fs.access(filePath);

const fileContent = await fs.readFile(filePath, "utf-8");

return new Response(fileContent, {
status: 200,
headers: { "Content-Type": "text/markdown; charset=utf-8" },
});
} catch (error: any) {
if (error.code === "ENOENT") {
return new Response("Not Found", { status: 404 });
} else {
return new Response("Internal Server Error", { status: 500 });
}
}
}
61 changes: 61 additions & 0 deletions app/(home)/components/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Link from "fumadocs-core/link";
import { cn } from "@/lib/utils";
import { InteractiveBadge } from "./interactive-badge";

interface CardProps {
href: string;
icon: React.ReactNode;
title: string;
description: string;
badges: Array<{
label: string;
href: string;
}>;
variant?: "default" | "secondary";
}

export function Card({
href,
icon,
title,
description,
badges,
variant = "default",
}: CardProps) {
return (
<div
className={cn(
"relative rounded-[0.6rem] p-[1.5px] overflow-hidden transition-colors hover:shadow-[0_6px_20px_rgba(89,86,80,0.2)] dark:hover:shadow-[0_6px_40px_#383432]",
{
"bg-gradient-to-br from-border via-border to-neutral-300 dark:to-neutral-200":
variant === "default",
"bg-gradient-to-br from-border via-border to-orange-500 dark:to-orange-700":
variant === "secondary",
}
)}
>
<Link
href={href}
className="not-prose block rounded-lg bg-[#ebe9e5] dark:bg-neutral-600 p-8 text-md text-neutral-500 dark:text-neutral-300 h-full"
>
<div className="mb-6">
<div className="w-20 h-20 rounded-lg flex items-center justify-center mb-4">
{icon}
</div>
<h3 className="text-2xl font-semibold mb-2">{title}</h3>
<p className="text-muted-foreground">{description}</p>
</div>

<div className="flex flex-wrap gap-2">
{badges?.map((badge) => (
<InteractiveBadge
key={badge.label}
href={badge.href}
label={badge.label}
/>
))}
</div>
</Link>
</div>
);
}
26 changes: 26 additions & 0 deletions app/(home)/components/interactive-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { Badge } from "@/components/ui/badge";

interface InteractiveBadgeProps {
href: string;
label: string;
}

export function InteractiveBadge({ href, label }: InteractiveBadgeProps) {
return (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
window.location.href = href;
}}
type="button"
className="hover:no-underline"
>
<Badge className="bg-neutral-50 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-300 hover:bg-neutral-200 hover:text-primary dark:hover:text-primary dark:hover:bg-neutral-950 cursor-pointer transition-colors">
{label}
</Badge>
</button>
);
}
3 changes: 2 additions & 1 deletion app/(home)/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { JSX } from "react";
import type { SVGProps } from "react";

export function HiroPlatformSVG(props: SVGProps<SVGSVGElement>): JSX.Element {
export function HiroPlatformSVG(props: SVGProps<SVGSVGElement>) {
return (
<svg width="474" height="40" viewBox="0 0 474 40" fill="none" {...props}>
<path
18 changes: 8 additions & 10 deletions app/(home)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { Layout } from "fumadocs-ui/layout";
import type { ReactNode } from "react";
import { homeLayoutOptions } from "../(docs)/layout";
import { HomeLayout } from "@/components/home";
import { TopNav } from "@/components/top-nav";
import { baseOptions } from "@/app/layout.config";

export default function HomeLayout({
children,
}: {
children: ReactNode;
}): JSX.Element {
export default function Layout({ children }: { children: ReactNode }) {
return (
<div className="px-10 *:border-none">
<Layout {...homeLayoutOptions}>{children}</Layout>
</div>
<>
<TopNav />
<HomeLayout {...baseOptions}>{children}</HomeLayout>
</>
);
}
103 changes: 35 additions & 68 deletions app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,49 @@
import Link from "fumadocs-core/link";
import { StacksCardIcon, BitcoinCardIcon } from "@/components/ui/icon";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Card as LinkCard } from "@/app/(home)/components/card";

export default function HomePage(): JSX.Element {
export default function HomePage() {
return (
<main className="container mx-auto my-12 space-y-10">
<main className="container mx-auto my-6 space-y-10">
<div className="space-y-1">
<h1 className="text-4xl font-bold text-[#141312] dark:text-[#f6f5f3]">
Welcome to Hiro Docs.
</h1>
<h2 className="text-2xl font-regular text-muted-foreground font-inter">
<h2 className="text-2xl font-regular text-muted-foreground">
Explore our tutorials, guides, API references, and more.
</h2>
</div>

<div className="grid md:grid-cols-2 gap-6 mb-16">
<Link
<LinkCard
href="/stacks"
className="not-prose block rounded-lg border bg-[#EBE9E6] dark:bg-[#2a2726] p-4 text-md text-card-foreground transition-colors hover:shadow-[0_6px_20px_rgba(89,86,80,0.2)] dark:hover:shadow-[0_6px_40px_#383432]"
>
<div className="mb-6">
<div className="w-20 h-20 rounded-lg flex items-center justify-center mb-4">
<StacksCardIcon />
</div>
<h3 className="text-xl font-semibold mb-2">Stacks Docs</h3>
<p className="text-muted-foreground">Start building on Stacks.</p>
</div>

<div className="flex flex-wrap gap-2">
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
CLARINET
</Badge>
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
CHAINHOOK
</Badge>
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
STACKS.JS
</Badge>
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
HIRO PLATFORM
</Badge>
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
STACKS API
</Badge>
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
TOKEN METADATA API
</Badge>
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
+3 MORE
</Badge>
</div>
</Link>
<Link
icon={<StacksCardIcon />}
title="Stacks Docs"
description="Start building on Stacks."
badges={[
{ label: "CLARINET", href: "/stacks/clarinet" },
{ label: "CHAINHOOK", href: "/stacks/chainhook" },
{ label: "APIs", href: "/stacks/api" },
{ label: "SDKs & LIBRARIES", href: "/stacks/reference" },
{ label: "HIRO PLATFORM", href: "/stacks/platform" },
{ label: "STACKS.JS", href: "/stacks/stacks.js" },
{ label: "STACKS CONNECT", href: "/stacks/connect" },
{ label: "CLARITY LANG", href: "/stacks/clarity" },
]}
/>
<LinkCard
variant="secondary"
href="/bitcoin"
className="not-prose block rounded-lg border bg-[#EBE9E6] dark:bg-[#2a2726] p-4 text-md text-card-foreground transition-colors hover:shadow-[0_6px_20px_rgba(89,86,80,0.2)] dark:hover:shadow-[0_6px_40px_#383432]"
>
<div className="mb-6">
<div className="w-20 h-20 rounded-lg flex items-center justify-center mb-4">
<BitcoinCardIcon />
</div>
<h3 className="text-xl font-semibold mb-2">Bitcoin Docs</h3>
<p className="text-muted-foreground">
Start building on Ordinals and Runes.
</p>
</div>
<div className="flex flex-wrap gap-2">
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
BITCOIN INDEXER
</Badge>
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
ORDINALS API
</Badge>
<Badge className="bg-[#f6f5f3] dark:bg-[#181717] text-primary dark:text-[#8c877d]">
RUNES API
</Badge>
</div>
</Link>
icon={<BitcoinCardIcon />}
title="Bitcoin Docs"
description="Start building on Ordinals and Runes."
badges={[
{ label: "BITCOIN INDEXER", href: "/bitcoin/indexer" },
{ label: "APIs", href: "/bitcoin/api" },
{ label: "SDKs & LIBRARIES", href: "/bitcoin/sdks" },
]}
/>
</div>

<main className="space-y-6">
@@ -89,7 +56,7 @@ export default function HomePage(): JSX.Element {

<div className="grid md:grid-cols-4 gap-6">
<Link href="/stacks/get-started" className="block h-full">
<Card className="p-6 border bg-[#f2f0ed] dark:bg-[#1e1c1b] h-full flex flex-col">
<Card className="p-6 border bg-neutral-100 dark:bg-neutral-700 h-full flex flex-col">
<h3 className="text-lg font-semibold mb-2">
Get started with Stacks
</h3>
@@ -100,7 +67,7 @@ export default function HomePage(): JSX.Element {
</Card>
</Link>
<Link href="/stacks/api" className="block h-full">
<Card className="p-6 border bg-[#f2f0ed] dark:bg-[#1e1c1b] h-full flex flex-col">
<Card className="p-6 border bg-neutral-100 dark:bg-neutral-700 h-full flex flex-col">
<h3 className="text-lg font-semibold mb-2">
Stacks API Overview
</h3>
@@ -110,7 +77,7 @@ export default function HomePage(): JSX.Element {
</Card>
</Link>
<Link href="/bitcoin/get-started" className="block h-full">
<Card className="p-6 border bg-[#f2f0ed] dark:bg-[#1e1c1b] h-full flex flex-col">
<Card className="p-6 border bg-neutral-100 dark:bg-neutral-700 h-full flex flex-col">
<h3 className="text-lg font-semibold mb-2">
Get started with Bitcoin
</h3>
@@ -121,8 +88,8 @@ export default function HomePage(): JSX.Element {
</Card>
</Link>
<Link href="/bitcoin/ordinals/api" className="block h-full">
<Card className="p-6 border bg-[#f2f0ed] dark:bg-[#1e1c1b] h-full flex flex-col">
<h3 className="text-lg font-semibold mb-2">
<Card className="p-6 border bg-neutral-100 dark:bg-neutral-700 h-full flex flex-col">
<h3 className="text-lg font-semibold mb-2 font-fono">
Ordinals API Overview
</h3>
<p className="text-muted-foreground text-sm flex-grow">
103 changes: 103 additions & 0 deletions app/api/llms/[[...section]]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as fs from "node:fs/promises";
import path from "node:path";
import fg from "fast-glob";
import matter from "gray-matter";
import { NextRequest } from "next/server";

const DOCS_CONTENT_PATH = path.join(process.cwd(), "content/docs");
const BASE_URL = "https://docs.hiro.so";

function generateTitle(section: string[] = []): string {
if (section.length === 0) {
return "# Hiro Documentation";
}
return `# ${section
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ")} Documentation`;
}

function getRawWebPath(filePath: string): string {
const relativePath = path.relative(DOCS_CONTENT_PATH, filePath);
const parsedPath = path.parse(relativePath);
let webPath = path.join("/raw", parsedPath.dir, parsedPath.name);
if (parsedPath.name === "index") {
webPath = path.dirname(webPath);

return webPath.replace(/\\/g, "/") + "/index.mdx";
}

return webPath.replace(/\\/g, "/") + ".mdx";
}

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ section?: string[] }> }
) {
const awaitedParams = await params;
let sectionSegments = awaitedParams.section || [];

if (
sectionSegments.length > 0 &&
sectionSegments[sectionSegments.length - 1] === "llms.txt"
) {
sectionSegments = sectionSegments.slice(0, -1);
}

const sectionPath = path.join(DOCS_CONTENT_PATH, ...sectionSegments);

try {
try {
await fs.access(sectionPath);
} catch (e) {
console.error(e);
return new Response("Not Found", { status: 404 });
}

const files = await fg(`${sectionPath}/*.mdx`, {
cwd: process.cwd(),
absolute: true,
onlyFiles: true,
});

const markdownOutput: string[] = [
generateTitle(sectionSegments),
"\n## Pages\n",
];

if (files.length === 0) {
markdownOutput.push("No pages found in this section.");
} else {
const fileDataPromises = files.map(async (file) => {
try {
const fileContent = await fs.readFile(file, "utf-8");
const { data } = matter(fileContent);
const title = data.title || path.basename(file, ".mdx"); // Fallback title
const description = data.description || "";
// Use the new function to get the /raw/ path
const rawWebPath = getRawWebPath(file);
const absoluteRawWebPath = `${BASE_URL}${rawWebPath}`; // Create absolute URL
return `- [${title}](${absoluteRawWebPath})${description ? `: ${description}` : ""}`;
} catch (readError) {
console.error(`Error processing file ${file}:`, readError);
return null; // Skip files that cause errors
}
});

const formattedFiles = (await Promise.all(fileDataPromises)).filter(
Boolean
) as string[];
formattedFiles.sort(); // Sort alphabetically by title/path
markdownOutput.push(...formattedFiles);
}

return new Response(markdownOutput.join("\n"), {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
} catch (error) {
console.error(
`Error generating llms.txt for /${sectionSegments.join("/")}:`,
error
);
return new Response("Internal Server Error", { status: 500 });
}
}
17 changes: 7 additions & 10 deletions app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { utils } from "@/utils/source";
import { createSearchAPI } from "fumadocs-core/search/server";
import { source } from "@/lib/source";
import { createFromSource } from "fumadocs-core/search/server";

export const { GET } = createSearchAPI("advanced", {
indexes: utils.getPages().map((page) => ({
title: page.data.title,
structuredData: page.data.exports.structuredData,
id: page.url,
url: page.url,
})),
});
// Cache forever for static export
export const revalidate = 0; // Use 0 instead of false for Next.js 15+

// Export the static GET handler
export const { staticGET: GET } = createFromSource(source);
53 changes: 53 additions & 0 deletions app/config/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { MainNavItem, SidebarNavItem } from "@/types/nav";

interface DocsConfig {
mainNav: MainNavItem[];
sidebarNav: SidebarNavItem[];
}

export const docsConfig: DocsConfig = {
mainNav: [
{
title: "Documentation",
href: "/docs",
},
{
title: "Cookbook",
href: "/cookbook",
},
{
title: "GitHub",
href: "https://github.com/example/repo", // Example external link
external: true,
},
],
sidebarNav: [
{
title: "Getting Started",
items: [
{
title: "Introduction",
href: "/docs/introduction",
},
{
title: "Installation",
href: "/docs/installation",
},
],
},
{
title: "Core Concepts",
items: [
{
title: "Concept A",
href: "/docs/concepts/a",
},
{
title: "Concept B",
href: "/docs/concepts/b",
},
],
},
// Add more sidebar groups as needed
],
};
47 changes: 23 additions & 24 deletions app/cookbook/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { JSX } from "react";
import { Code } from "@/components/docskit/code";
import { loadRecipe, loadRecipes } from "@/utils/loader";
import { loadRecipes } from "@/lib/loader";
import { Badge } from "@/components/ui/badge";
import { HoverProvider } from "@/context/hover";
import { docskit } from "@/components/docskit/components";
import { HoverLink } from "@/components/docskit/annotations/hover";
import { Terminal } from "@/components/docskit/terminal";
import { InlineCode } from "@/components/docskit/inline-code";
import { WithNotes } from "@/components/docskit/notes";
import { SnippetResult } from "../components/snippet-result";
import Link from "next/link";
import { RecipeCarousel } from "@/components/recipe-carousel";
@@ -16,30 +17,16 @@ import {
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Metadata } from "next/types";
import { getRouteMetadata, createMetadata } from "@/utils/metadata";

interface Param {
id: string;
}
import { createMetadata } from "@/lib/metadata";
import { source } from "@/lib/source";
import { getRouteMetadata } from "@/lib/metadata";

export const dynamicParams = false;

export async function generateMetadata({
params,
}: {
params: { id: string };
}): Promise<Metadata> {
const routeMetadata = getRouteMetadata("/cookbook");
return createMetadata(routeMetadata);
}

export default async function Page({
params,
}: {
params: Param;
export default async function Page(props: {
params: Promise<{ id: string }>;
}): Promise<JSX.Element> {
const { id } = params;
const { id } = await props.params;

const recipes = await loadRecipes();
const recipe = recipes.find((r) => r.id === id);
@@ -91,7 +78,7 @@ export default async function Page({
Terminal,
Code,
InlineCode,
WithNotes,
...docskit,
}}
/>
</div>
@@ -116,7 +103,7 @@ export default async function Page({
Terminal,
Code,
InlineCode,
WithNotes,
...docskit,
}}
/>
</div>
@@ -156,3 +143,15 @@ export default async function Page({
</>
);
}

export async function generateStaticParams() {
return source.generateParams();
}

export async function generateMetadata(props: {
params: Promise<{ id: string }>;
}) {
const { id } = await props.params;
const routeMetadata = getRouteMetadata(`/cookbook/${id}`);
return createMetadata(routeMetadata);
}
4 changes: 2 additions & 2 deletions app/cookbook/components/cookbook-ui.tsx
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

import { useState, useMemo, Suspense, useEffect, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Recipe } from "@/types/recipes";
import { Recipe } from "@/types";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -299,7 +299,7 @@ function CookbookContent({ initialRecipes, recipeCards }: CookbookProps) {
{view === "list" ? (
<Table>
<TableBody>
{filteredRecipeCards.map(({ recipe, card }, index) => {
{filteredRecipeCards.map(({ recipe }, index) => {
const isLastItem = index === filteredRecipeCards.length - 1;

return (
164 changes: 85 additions & 79 deletions app/cookbook/components/snippet-result.tsx
Original file line number Diff line number Diff line change
@@ -2,16 +2,16 @@

import React from "react";
import Link from "next/link";
import { Play, Terminal } from "lucide-react";
import { Button } from "@/components/ui/button";
import { buttonVariants } from "@/components/ui/button";
import { ArrowUpRight } from "lucide-react";
import { Code } from "@/components/docskit/code";
import { initSimnet, type Simnet } from "@hirosystems/clarinet-sdk-browser";
// import { Code } from "@/components/docskit/code";
import type { Simnet } from "@hirosystems/clarinet-sdk-browser";
// import { initSimnet } from "@hirosystems/clarinet-sdk-browser";
import { Cl } from "@stacks/transactions";
import { loadSandpackClient } from "@codesandbox/sandpack-client";
import type { SandboxSetup } from "@codesandbox/sandpack-client";
// import { loadSandpackClient } from "@codesandbox/sandpack-client";
// import type { SandboxSetup } from "@codesandbox/sandpack-client";

import type { Recipe } from "@/types/recipes";
import type { Recipe } from "@/types";

interface SnippetResultProps {
recipe: Recipe;
@@ -62,71 +62,71 @@ export function SnippetResult({
}
}, [isConsoleOpen]);

async function runCode() {
if (type === "clarity") {
if (isConsoleOpen) {
setIsConsoleOpen(false);
return;
}
setIsLoading(true);
setResult(null);
// async function runCode() {
// if (type === "clarity") {
// if (isConsoleOpen) {
// setIsConsoleOpen(false);
// return;
// }
// setIsLoading(true);
// setResult(null);

try {
const simnet = await initSimnet();
await simnet.initEmptySession(false);
simnet.deployer = "ST000000000000000000002AMW42H";
const deployer = simnet.deployer;
console.log("deployer", deployer);
simnet.setEpoch("3.0");
// try {
// const simnet = await initSimnet();
// await simnet.initEmptySession(false);
// simnet.deployer = "ST000000000000000000002AMW42H";
// const deployer = simnet.deployer;
// // console.log("deployer", deployer);
// simnet.setEpoch("3.0");

// Store the initialized simnet instance
setSimnetInstance(simnet);
// Store the initial code in history
setCodeHistory(code);
// // Store the initialized simnet instance
// setSimnetInstance(simnet);
// // Store the initial code in history
// setCodeHistory(code);

const contract = simnet.deployContract(
recipe.files[0].name.split(".")[0],
code,
{ clarityVersion: 3 },
deployer
);
const result = contract.result;
const prettyResult = Cl.prettyPrint(result, 2);
// console.log("before :", simnet.execute("stacks-block-height"));
// simnet.executeCommand("::advance_chain_tip 2");
// console.log("after: ", simnet.execute("stacks-block-height"));
// const contract = simnet.deployContract(
// recipe.files[0].name.split(".")[0],
// code,
// { clarityVersion: 3 },
// deployer
// );
// const result = contract.result;
// const prettyResult = Cl.prettyPrint(result, 2);
// // console.log("before :", simnet.execute("stacks-block-height"));
// // simnet.executeCommand("::advance_chain_tip 2");
// // console.log("after: ", simnet.execute("stacks-block-height"));

// Add a 1-second delay before updating the result
// await new Promise((resolve) => setTimeout(resolve, 1000));
// // Add a 1-second delay before updating the result
// // await new Promise((resolve) => setTimeout(resolve, 1000));

setResult(prettyResult);
setIsConsoleOpen(true);
} catch (error) {
console.error("Error running code snippet:", error);
setResult("An error occurred while running the code snippet.");
} finally {
setIsLoading(false);
}
} else {
const content = {
files: {
"/package.json": {
code: JSON.stringify({
main: "index.js",
dependencies: dependencies || {},
}),
},
"/index.js": {
code: code, // This is the content from your recipe file
},
},
environment: "vanilla",
};
// setResult(prettyResult);
// setIsConsoleOpen(true);
// } catch (error) {
// console.error("Error running code snippet:", error);
// setResult("An error occurred while running the code snippet.");
// } finally {
// setIsLoading(false);
// }
// } else {
// const content = {
// files: {
// "/package.json": {
// code: JSON.stringify({
// main: "index.js",
// dependencies: dependencies || {},
// }),
// },
// "/index.js": {
// code: code, // This is the content from your recipe file
// },
// },
// environment: "vanilla",
// };

const client = await loadSandpackClient(iframeRef.current!, content);
console.log(client);
}
}
// // const client = await loadSandpackClient(iframeRef.current!, content);
// // console.log(client);
// }
// }

// Add this function to handle keyboard events
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -199,14 +199,14 @@ export function SnippetResult({
}
}

const getButtonText = () => {
if (type === "clarity") {
if (isLoading) return "Loading...";
if (isConsoleOpen) return "Close terminal";
return "Open in terminal";
}
return "Run code snippet";
};
// const getButtonText = () => {
// if (type === "clarity") {
// if (isLoading) return "Loading...";
// if (isConsoleOpen) return "Close terminal";
// return "Open in terminal";
// }
// return "Run code snippet";
// };

return (
<div className="space-y-4">
@@ -231,11 +231,17 @@ export function SnippetResult({
{getButtonText()}
</Button> */}
{type === "clarity" && (
<Button variant="link" className="gap-2 self-end" size="sm" asChild>
<Link href={recipe?.external_url || ""} target="_blank">
Open in Clarity Playground <ArrowUpRight className="w-4 h-4" />
</Link>
</Button>
<Link
href={recipe?.external_url || ""}
target="_blank"
className={buttonVariants({
size: "sm",
className:
"gap-2 self-end text-primary underline-offset-4 hover:underline",
})}
>
Open in Clarity Playground <ArrowUpRight className="w-4 h-4" />
</Link>
)}
</div>
{result && type !== "clarity" && (
13 changes: 6 additions & 7 deletions app/cookbook/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Layout } from "fumadocs-ui/layout";
import React, { JSX } from "react";
import type { ReactNode } from "react";
import { homeLayoutOptions } from "../(docs)/layout";
import { TopNav } from "@/components/top-nav";

export default function CookbookLayout({
children,
@@ -9,11 +9,10 @@ export default function CookbookLayout({
}): JSX.Element {
return (
<div className="px-10 *:border-none">
<Layout {...homeLayoutOptions}>
<div className="min-h-screen py-8">
<div className="space-y-4">{children}</div>
</div>
</Layout>
<TopNav />
<div className="min-h-screen py-8">
<div className="space-y-4">{children}</div>
</div>
</div>
);
}
6 changes: 3 additions & 3 deletions app/cookbook/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { loadRecipes } from "@/utils/loader";
import { loadRecipes } from "@/lib/loader";
import { CookbookUI } from "./components/cookbook-ui";
import { Code } from "@/components/docskit/code";
import { Recipe } from "@/types/recipes";
import { Recipe } from "@/types";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { CopyButton } from "@/components/docskit/copy-button";
import { Metadata } from "next/types";
import { getRouteMetadata, createMetadata } from "@/utils/metadata";
import { getRouteMetadata, createMetadata } from "@/lib/metadata";

function RecipeCard({
recipe,
Binary file removed app/favicon.ico
Binary file not shown.
839 changes: 268 additions & 571 deletions app/global.css

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions app/layout.config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
/**
* Shared layout configurations
*
* you can customise layouts individually from:
* Home Layout: app/(home)/layout.tsx
* Docs Layout: app/docs/layout.tsx
*/
export const baseOptions: BaseLayoutProps = {
nav: {
enabled: false,
},
links: [],
};
151 changes: 20 additions & 131 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,143 +1,32 @@
import "./global.css";
import { aeonik, aeonikFono, aeonikMono, inter } from "@/app/fonts";
import type { Viewport } from "next";
import { baseUrl, createMetadata } from "@/utils/metadata";
import { Provider } from "./provider";
import { GoogleTagManager } from "@next/third-parties/google";
import { Banner } from "@/components/ui/banner";
import { Discord, Github, HiroSVG, Youtube, X } from "@/components/ui/icon";
import { RootProvider } from "fumadocs-ui/provider";
import type { ReactNode } from "react";
import { aeonik, aeonikFono, aeonikMono, inter } from "@/fonts";
import { SearchProvider } from "@/lib/hooks/use-search";
import SearchDialog from "@/components/search-dialog";
import { KeyboardShortcutsProvider } from "@/lib/hooks/use-keyboard-shortcuts";

const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID as string;

export const metadata = createMetadata({
title: {
template: "%s | Hiro Docs",
default: "Hiro Documentation",
},
description: "Hiro Documentation",
metadataBase: baseUrl,
});

export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: dark)", color: "var(--background)" },
{ media: "(prefers-color-scheme: light)", color: "var(--background)" },
],
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html
lang="en"
className={`${aeonik.variable} ${aeonikFono.variable} ${aeonikMono.variable} ${inter.variable}`}
suppressHydrationWarning
>
<GoogleTagManager gtmId={GTM_ID} />
<body className="flex min-h-screen flex-col">
<Provider>
<Banner
id="api-tiers"
cta="Meet Hiro’s new account tiers"
url="https://platform.hiro.so/pricing"
startDate="2025-04-09"
endDate="2025-04-16"
mobileText="Increase your API rate limits"
>
Increased API rate limits, dedicated support channels.
</Banner>
{children}
<Footer />
</Provider>
<body className="flex flex-col min-h-screen">
<KeyboardShortcutsProvider>
<SearchProvider>
<RootProvider
search={{
enabled: false,
}}
>
{children}
</RootProvider>
<SearchDialog />
</SearchProvider>
</KeyboardShortcutsProvider>
</body>
</html>
);
}

function Footer(): JSX.Element {
return (
<footer className="mt-auto border-t border-accent bg-background py-12 text-secondary-foreground">
<div className="container flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="flex flex-row justify-between items-center w-full">
<div className="flex flex-row items-center gap-6">
<div className="bg-primary w-fit rounded-[4px] p-1.5 text-muted-foreground [&_svg]:size-4">
<HiroSVG className="text-card" />
</div>
<a
className="text-sm text-primary transition-colors hover:text-accent-foreground max-lg:hidden after:content-[''] after:block after:h-[1px] after:mt-0.5 after:border after:border-1 after:border-border hover:after:border-primary/50"
href="https://hiro.so/"
target="_blank"
>
hiro.so
</a>
<a
className="text-sm text-primary transition-colors hover:text-accent-foreground max-lg:hidden after:content-[''] after:block after:h-[1px] after:mt-0.5 after:border after:border-1 after:border-border hover:after:border-primary/50"
href="/guides"
>
Guides
</a>
<a
className="text-sm text-primary transition-colors hover:text-accent-foreground max-lg:hidden after:content-[''] after:block after:h-[1px] after:mt-0.5 after:border after:border-1 after:border-border hover:after:border-primary/50"
href="https://platform.hiro.so/"
target="_blank"
>
Hiro Platform
</a>
<a
className="text-sm text-primary transition-colors hover:text-accent-foreground max-lg:hidden after:content-[''] after:block after:h-[1px] after:mt-0.5 after:border after:border-1 after:border-border hover:after:border-primary/50"
href="https://status.hiro.so/"
target="_blank"
>
Status
</a>
<a
className="text-sm text-primary transition-colors hover:text-accent-foreground max-lg:hidden after:content-[''] after:block after:h-[1px] after:mt-0.5 after:border after:border-1 after:border-border hover:after:border-primary/50"
href="https://hackerone.com/hiro?type=team"
target="_blank"
>
Bounty Program
</a>
</div>
<div className="flex flex-col space-y-3 items-end">
<div className="flex flex-row items-center gap-6">
<a
href="https://x.com/hirosystems"
target="_blank"
className="transition-colors"
>
<X />
</a>
<a
href="https://stacks.chat/"
target="_blank"
className="transition-colors"
>
<Discord />
</a>
<a
href="https://github.com/hirosystems"
target="_blank"
className="transition-colors"
>
<Github />
</a>
<a
href="https://www.youtube.com/c/hirosystems/"
target="_blank"
className="transition-colors"
>
<Youtube />
</a>
</div>
<p className="text-sm text-[#b7ac9f] font-aeonikFono">
Copyright &copy; {new Date().getFullYear()} Hiro Systems, PBC.
</p>
</div>
</div>
</div>
</footer>
);
}
44 changes: 44 additions & 0 deletions app/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as fs from "node:fs/promises";
import fg from "fast-glob";
import matter from "gray-matter";
import { remark } from "remark";
import remarkGfm from "remark-gfm";
import remarkStringify from "remark-stringify";
import remarkMdx from "remark-mdx";
import { remarkInclude } from "fumadocs-mdx/config";

export const revalidate = false;

export async function GET() {
// all scanned content
const files = await fg(["./content/docs/**/*.mdx"]);

const scan = files.map(async (file) => {
const fileContent = await fs.readFile(file);
const { content, data } = matter(fileContent.toString());

const processed = await processContent(content);
return `file: ${file}
meta: ${JSON.stringify(data, null, 2)}
${processed}`;
});

const scanned = await Promise.all(scan);

return new Response(scanned.join("\n\n"));
}

async function processContent(content: string): Promise<string> {
const file = await remark()
.use(remarkMdx)
// https://fumadocs.vercel.app/docs/mdx/include
.use(remarkInclude)
// gfm styles
.use(remarkGfm)
// .use(your remark plugins)
.use(remarkStringify) // to string
.process(content);

return String(file);
}
21 changes: 0 additions & 21 deletions app/provider.tsx

This file was deleted.

33 changes: 0 additions & 33 deletions app/sitemap.ts

This file was deleted.

388 changes: 388 additions & 0 deletions app/theme.css

Large diffs are not rendered by default.

1,713 changes: 1,713 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions components.json
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"config": "",
"css": "app/global.css",
"baseColor": "neutral",
"cssVariables": true,
@@ -16,5 +16,6 @@
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
},
"iconLibrary": "lucide"
}
185 changes: 185 additions & 0 deletions components/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"use client";

import * as React from "react";
import {
BookOpen,
Bot,
Command,
Frame,
LifeBuoy,
Map,
PieChart,
Send,
Settings2,
SquareTerminal,
} from "lucide-react";

import { NavMain } from "@/components/nav-main";
import { NavProjects } from "@/components/nav-projects";
import { NavSecondary } from "@/components/nav-secondary";
import { NavUser } from "@/components/nav-user";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";

const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Playground",
url: "#",
icon: SquareTerminal,
isActive: true,
items: [
{
title: "History",
url: "#",
},
{
title: "Starred",
url: "#",
},
{
title: "Settings",
url: "#",
},
],
},
{
title: "Models",
url: "#",
icon: Bot,
items: [
{
title: "Genesis",
url: "#",
},
{
title: "Explorer",
url: "#",
},
{
title: "Quantum",
url: "#",
},
],
},
{
title: "Documentation",
url: "#",
icon: BookOpen,
items: [
{
title: "Introduction",
url: "#",
},
{
title: "Get Started",
url: "#",
},
{
title: "Tutorials",
url: "#",
},
{
title: "Changelog",
url: "#",
},
],
},
{
title: "Settings",
url: "#",
icon: Settings2,
items: [
{
title: "General",
url: "#",
},
{
title: "Team",
url: "#",
},
{
title: "Billing",
url: "#",
},
{
title: "Limits",
url: "#",
},
],
},
],
navSecondary: [
{
title: "Support",
url: "#",
icon: LifeBuoy,
},
{
title: "Feedback",
url: "#",
icon: Send,
},
],
projects: [
{
name: "Design Engineering",
url: "#",
icon: Frame,
},
{
name: "Sales & Marketing",
url: "#",
icon: PieChart,
},
{
name: "Travel",
url: "#",
icon: Map,
},
],
};

export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar variant="inset" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<a href="#">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<Command className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Acme Inc</span>
<span className="truncate text-xs">Enterprise</span>
</div>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
);
}
2 changes: 1 addition & 1 deletion components/badge.tsx
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ export function HackBadge({ variant }: BadgeProps) {
className={cn(
"inline-flex items-center gap-1.5 px-2 py-1 rounded-md border",
variantStyles[variant],
"text-sm font-aeonikFono"
"text-sm font-aeonik-fono"
)}
>
<Icon className="w-4 h-4" />
40 changes: 40 additions & 0 deletions components/banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type React from "react";
import Link from "next/link";
import { AlertCircle } from "lucide-react";

interface BannerProps {
message: string;
link?: {
text: string;
href: string;
};
icon?: React.ReactNode;
}

export function Banner({
message,
link,
icon = <AlertCircle className="h-4 w-4" />,
}: BannerProps) {
return (
<div className="w-full bg-primary/10 py-2 text-center text-sm">
<div className="container flex items-center justify-center gap-1.5">
{icon}
<p className="font-medium">
{message}
{link && (
<>
{" "}
<Link
href={link.href}
className="font-semibold underline underline-offset-4 hover:text-primary"
>
{link.text}
</Link>
</>
)}
</p>
</div>
</div>
);
}
21 changes: 13 additions & 8 deletions components/bento-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ArrowRightIcon } from "lucide-react";
import { ArrowRightIcon } from "@radix-ui/react-icons";
import { ReactNode } from "react";

const BentoGrid = ({
@@ -64,12 +64,17 @@ const BentoCard = ({
"pointer-events-none absolute bottom-0 flex w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100"
)}
>
<Button variant="ghost" size="sm" className="pointer-events-auto">
<a href={href}>
{cta}
<ArrowRightIcon className="ml-2 h-4 w-4" />
</a>
</Button>
<a
href={href}
className={buttonVariants({
size: "sm",
className:
"pointer-events-auto hover:bg-accent hover:text-accent-foreground",
})}
>
{cta}
<ArrowRightIcon className="ml-2 h-4 w-4" />
</a>
</div>
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
</div>
2 changes: 1 addition & 1 deletion components/callout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AlertOctagon, AlertTriangle, Info, Star } from "lucide-react";
import { AlertTriangle, Info, Star } from "lucide-react";
import { HiroSVG } from "@/components/ui/icon";
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
import { cn } from "@/lib/utils";
5 changes: 2 additions & 3 deletions components/code/connect-wallet-button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import React from "react";
import { isConnected, connect, disconnect } from "@stacks/connect";
import { connect, disconnect } from "@stacks/connect";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

@@ -10,9 +10,8 @@ export const ConnectWalletButton: React.FC = () => {

async function authenticate() {
try {
const response = await connect();
await connect();
setIsSignedIn(true);
console.log(response);
} catch (error) {
console.error("Authentication failed:", error);
setIsSignedIn(false);
126 changes: 126 additions & 0 deletions components/command-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import * as React from "react";
import { useRouter } from "next/navigation";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Circle, File, Laptop, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";

import { docsConfig } from "@/app/config/docs";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";

export function CommandMenu({ ...props }: DialogProps) {
const router = useRouter();
const [open, setOpen] = React.useState(false);
const { setTheme } = useTheme();

React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || e.key === "/") {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return;
}

e.preventDefault();
setOpen((open) => !open);
}
};

document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);

const runCommand = React.useCallback((command: () => unknown) => {
setOpen(false);
command();
}, []);

return (
<>
<Button
className={cn(
"relative h-8 w-full justify-start rounded-[0.5rem] bg-background text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-56 xl:w-64",
"border border-input hover:bg-accent hover:text-accent-foreground"
)}
onClick={() => setOpen(true)}
{...props}
>
<span className="hidden lg:inline-flex">Search documentation...</span>
<span className="inline-flex lg:hidden">Search...</span>
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs"></span>K
</kbd>
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Links">
{docsConfig.mainNav
.filter((navitem) => !navitem.external)
.map((navItem) => (
<CommandItem
key={navItem.href}
value={navItem.title}
onSelect={() => {
runCommand(() => router.push(navItem.href as string));
}}
>
<File />
{navItem.title}
</CommandItem>
))}
</CommandGroup>
{docsConfig.sidebarNav.map((group) => (
<CommandGroup key={group.title} heading={group.title}>
{group.items.map((navItem) => (
<CommandItem
key={navItem.href}
value={navItem.title}
onSelect={() => {
runCommand(() => router.push(navItem.href as string));
}}
>
<div className="mr-2 flex h-4 w-4 items-center justify-center">
<Circle className="h-3 w-3" />
</div>
{navItem.title}
</CommandItem>
))}
</CommandGroup>
))}
<CommandSeparator />
<CommandGroup heading="Theme">
<CommandItem onSelect={() => runCommand(() => setTheme("light"))}>
<Sun />
Light
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme("dark"))}>
<Moon />
Dark
</CommandItem>
<CommandItem onSelect={() => runCommand(() => setTheme("system"))}>
<Laptop />
System
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</>
);
}
285 changes: 0 additions & 285 deletions components/components-sidebar-05.tsx

This file was deleted.

79 changes: 0 additions & 79 deletions components/dialog.tsx

This file was deleted.

49 changes: 49 additions & 0 deletions components/docs.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { Menu, X } from "lucide-react";
import { type ButtonHTMLAttributes, type HTMLAttributes } from "react";
import { cn } from "../lib/utils";
import { buttonVariants } from "./ui/button";
import { useSidebar } from "fumadocs-ui/provider";
import { useNav } from "./layout/nav";
import { SidebarTrigger } from "fumadocs-core/sidebar";

export function Navbar(props: HTMLAttributes<HTMLElement>) {
const { open } = useSidebar();
const { isTransparent } = useNav();

return (
<header
id="nd-subnav"
{...props}
className={cn(
"sticky top-(--fd-banner-height) z-30 flex h-14 flex-row items-center border-b border-fd-foreground/10 px-4 backdrop-blur-lg transition-colors",
(!isTransparent || open) && "bg-fd-background/80",
props.className
)}
>
{props.children}
</header>
);
}

export function NavbarSidebarTrigger(
props: ButtonHTMLAttributes<HTMLButtonElement>
) {
const { open } = useSidebar();

return (
<SidebarTrigger
{...props}
className={cn(
buttonVariants({
variant: "ghost",
size: "icon",
}),
props.className
)}
>
{open ? <X /> : <Menu />}
</SidebarTrigger>
);
}
233 changes: 233 additions & 0 deletions components/docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import type { PageTree } from "fumadocs-core/server";
import { type ReactNode, type HTMLAttributes } from "react";
import Link from "next/link";
import { ChevronRight, Languages } from "lucide-react";
import { cn } from "../lib/utils";
import { buttonVariants } from "./ui/button";
import {
CollapsibleSidebar,
Sidebar,
SidebarFooter,
SidebarHeader,
SidebarCollapseTrigger,
SidebarViewport,
SidebarPageTree,
} from "./docs/sidebar";
import { replaceOrDefault } from "./shared";
import { type LinkItemType, BaseLinkItem } from "./links";
import { RootToggle } from "./layout/root-toggle";
import { type BaseLayoutProps, getLinks } from "./shared";
import { LanguageToggle, LanguageToggleText } from "./layout/language-toggle";
import { Navbar, NavbarSidebarTrigger } from "./docs.client";
import { TreeContextProvider } from "fumadocs-ui/provider";
import { NavProvider, Title } from "./layout/nav";
import { ThemeToggle } from "./layout/theme-toggle";
import { LargeSearchToggle, SearchToggle } from "./layout/search-toggle";
import {
checkPageTree,
getSidebarTabsFromOptions,
layoutVariables,
SidebarLinkItem,
type SidebarOptions,
} from "./docs/shared";
import { type PageStyles, StylesProvider } from "fumadocs-ui/provider";
import { EnhancedSidebarPageTree } from "./docs/custom-sidebar";

export interface DocsLayoutProps extends BaseLayoutProps {
tree: PageTree.Root;

sidebar?: Partial<SidebarOptions>;

containerProps?: HTMLAttributes<HTMLDivElement>;
}

export function DocsLayout({
nav: {
enabled: navEnabled = true,
component: navReplace,
transparentMode,
...nav
} = {},
sidebar: {
enabled: sidebarEnabled = true,
collapsible = true,
component: sidebarReplace,
tabs: tabOptions,
banner: sidebarBanner,
footer: sidebarFooter,
components: sidebarComponents,
hideSearch: sidebarHideSearch,
...sidebar
} = {},
i18n = false,
children,
...props
}: DocsLayoutProps): ReactNode {
checkPageTree(props.tree);
const links = getLinks(props.links ?? [], props.githubUrl);
const Aside = collapsible ? CollapsibleSidebar : Sidebar;

const tabs = getSidebarTabsFromOptions(tabOptions, props.tree) ?? [];
const variables = cn(
"[--fd-tocnav-height:36px] md:[--fd-sidebar-width:268px] lg:[--fd-sidebar-width:286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px]",
!navReplace && navEnabled
? "[--fd-nav-height:calc(var(--spacing)*14)] md:[--fd-nav-height:0px]"
: undefined
);

const pageStyles: PageStyles = {
tocNav: cn("xl:hidden"),
toc: cn("max-xl:hidden"),
};

return (
<TreeContextProvider tree={props.tree}>
<NavProvider transparentMode={transparentMode}>
{replaceOrDefault(
{ enabled: navEnabled, component: navReplace },
<Navbar className="md:hidden">
<Title url={nav.url} title={nav.title} />
<div className="flex flex-1 flex-row items-center gap-1">
{nav.children}
</div>
<SearchToggle hideIfDisabled />
<NavbarSidebarTrigger className="-me-2 md:hidden" />
</Navbar>,
nav
)}
<main
id="nd-docs-layout"
{...props.containerProps}
className={cn(
"flex flex-1 flex-row pe-(--fd-layout-offset)",
variables,
props.containerProps?.className
)}
style={{
...layoutVariables,
...props.containerProps?.style,
}}
>
{collapsible ? (
<SidebarCollapseTrigger
className={cn(
buttonVariants({
variant: "secondary",
size: "icon",
}),
"fixed top-1/2 -translate-y-1/2 start-0 z-40 text-fd-muted-foreground rounded-s-none shadow-md data-[collapsed=false]:hidden max-md:hidden"
)}
>
<ChevronRight />
</SidebarCollapseTrigger>
) : null}
{replaceOrDefault(
{ enabled: sidebarEnabled, component: sidebarReplace },
<Aside
{...sidebar}
className={cn("md:ps-(--fd-layout-offset)", sidebar.className)}
>
{/* <SidebarHeader>
<div className="flex flex-row pt-1 max-md:hidden">
<Link
href={nav.url ?? "/"}
className="inline-flex text-[15px] items-center gap-2.5 font-medium"
>
{nav.title}
</Link>
{nav.children}
{collapsible && (
<SidebarCollapseTrigger
className={cn(
buttonVariants({
variant: "ghost",
size: "icon-sm",
}),
"ms-auto mb-auto text-fd-muted-foreground max-md:hidden"
)}
/>
)}
</div>
{sidebarBanner}
{tabs.length > 0 ? (
<RootToggle options={tabs} className="-mx-2" />
) : null}
{!sidebarHideSearch ? (
<LargeSearchToggle
hideIfDisabled
className="rounded-lg max-md:hidden"
/>
) : null}
</SidebarHeader> */}
<SidebarViewport>
<div className="mb-4 empty:hidden">
{links
.filter((v) => v.type !== "icon")
.map((item, i) => (
<SidebarLinkItem key={i} item={item} />
))}
</div>
<EnhancedSidebarPageTree components={sidebarComponents} />
</SidebarViewport>
{/* <SidebarFooter>
<SidebarFooterItems
links={links}
i18n={i18n}
disableThemeSwitch={props.disableThemeSwitch ?? false}
/>
{sidebarFooter}
</SidebarFooter> */}
</Aside>,
{
...sidebar,
tabs,
}
)}
<StylesProvider {...pageStyles}>{children}</StylesProvider>
</main>
</NavProvider>
</TreeContextProvider>
);
}

function SidebarFooterItems({
i18n,
disableThemeSwitch,
links,
}: {
i18n: boolean;
links: LinkItemType[];
disableThemeSwitch: boolean;
}) {
const iconItems = links.filter((v) => v.type === "icon");

// empty footer items
if (links.length === 0 && !i18n && disableThemeSwitch) return null;

return (
<div className="flex flex-row items-center">
{iconItems.map((item, i) => (
<BaseLinkItem
key={i}
item={item}
className={cn(
buttonVariants({ size: "icon", variant: "ghost" }),
"text-muted-foreground md:[&_svg]:size-4.5"
)}
aria-label={item.label}
>
{item.icon}
</BaseLinkItem>
))}
{/* {i18n ? (
<LanguageToggle className="me-1.5">
<Languages className="size-4.5" />
<LanguageToggleText className="md:hidden" />
</LanguageToggle>
) : null} */}
</div>
);
}

export { getSidebarTabsFromOptions, type TabOptions } from "./docs/shared";
export { type LinkItemType };
Loading