Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://ui.justinlevine.me">
<img src="./public/brand/repo-header.png" alt="jal-co/ui" />
<img src="./apps/docs/public/brand/repo-header.png" alt="jal-co/ui" />
</a>
</p>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { generateComponentPrompt } from "@/lib/prompts"
import { getRegistryItem } from "@/lib/registry"

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

Expand Down Expand Up @@ -101,14 +101,17 @@ export default async function Page(props: {
}

export function generateStaticParams() {
return source.generateParams()
// Filter out the root page — it's handled by the dedicated /docs/page.tsx
return source.generateParams().filter(
(p: { slug: string[] }) => p.slug.length > 0
)
}

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

Expand Down
3 changes: 0 additions & 3 deletions apps/docs/app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ export default function DocsLayout({ children }: { children: ReactNode }) {
<Button variant="ghost" size="sm" asChild>
<Link href="/docs">Components</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="/docs/releases">Releases</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="/docs/installation">Installation</Link>
</Button>
Expand Down
124 changes: 124 additions & 0 deletions apps/docs/app/docs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { Metadata } from "next"
import { readdirSync } from "node:fs"
import { join } from "node:path"
import Link from "next/link"
import { docsNav, getActiveBadge } from "@/lib/docs"
import { getRegistryItem } from "@/lib/registry"
import { ThemeImage } from "@/components/docs/theme-image"

export const metadata: Metadata = {
title: "Components — jal-co/ui",
description:
"Browse all jalco ui components. Polished, composable building blocks for React and Next.js.",
}

const PREVIEWS_DIR = join(process.cwd(), "public/previews")
const previewFiles = (() => {
try {
return readdirSync(PREVIEWS_DIR)
} catch {
return []
}
})()
const availableImages = new Set(
previewFiles
.filter((f) => f.endsWith("-dark.png"))
.map((f) => f.replace("-dark.png", ""))
)
const availableVideos = new Set(
previewFiles
.filter((f) => f.endsWith("-dark.webm"))
.map((f) => f.replace("-dark.webm", ""))
)

function getComponentDescription(href: string): string | null {
const slug = href.split("/").pop()
if (!slug) return null
const item = getRegistryItem(slug)
return item?.description ?? null
}

function getSlug(href: string): string {
return href.split("/").pop() ?? ""
}

export default function DocsPage() {
const componentGroups = docsNav.filter(
(group) => group.title !== "Getting Started"
)

const totalCount = componentGroups.reduce(
(sum, group) => sum + group.items.filter((i) => !i.bundledIn).length,
0
)

return (
<div className="flex flex-col gap-10 py-10 px-6 md:px-10">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold tracking-tight">Components</h1>
<p className="text-base text-muted-foreground">
{totalCount} polished, composable components ready to install and
adapt.
</p>
</div>

<div className="flex flex-col gap-10">
{componentGroups.map((group) => (
<section key={group.title} className="flex flex-col gap-4">
<h2 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
{group.title}
</h2>

<div className="grid gap-4 sm:grid-cols-2">
{group.items
.filter((item) => !item.bundledIn)
.map((item) => {
const description = getComponentDescription(item.href)
const badge = getActiveBadge(item)
const slug = getSlug(item.href)
const hasImage = availableImages.has(slug)
const hasVideo = availableVideos.has(slug)

return (
<Link
key={item.href}
href={item.href}
className="group relative flex flex-col overflow-hidden rounded-xl border border-border transition-colors hover:border-foreground/20 hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
>
{hasImage && (
<div className="relative overflow-hidden border-b border-border">
<ThemeImage
slug={slug}
title={item.title}
hasVideo={hasVideo}
/>
</div>
)}

<div className="flex flex-col gap-1.5 p-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">
{item.title}
</span>
{badge && (
<span className="rounded-full border border-dashed border-ring/40 px-1.5 py-0.5 text-[10px] font-medium leading-none text-ring">
{badge}
</span>
)}
</div>
{description && (
<p className="line-clamp-2 text-[13px] leading-relaxed text-muted-foreground">
{description}
</p>
)}
</div>
</Link>
)
})}
</div>
</section>
))}
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion apps/docs/lib/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const docsNav: NavGroup[] = [
title: "Getting Started",
items: [
{ title: "Components", href: "/docs" },
{ title: "Releases", href: "/docs/changelog" },
{ title: "Releases", href: "/docs/releases" },
{ title: "Installation", href: "/docs/installation" },
{ title: "Color Themes", href: "/docs/themes" },
{ title: "llms.txt", href: "/llms.txt" },
Expand Down
Loading