"use client";
import { useEffect, useState, type ReactNode } from "react";
export type NavigationItem = { href: string; label: string; icon?: ReactNode };
export function Breadcrumbs({
items,
label = "Breadcrumb",
className = "",
}: {
items: readonly { label: string; href?: string }[];
label?: string;
className?: string;
}) {
return (
);
}
export function Pagination({
page,
totalPages,
onPageChange,
label = "Pagination",
className = "",
}: {
page: number;
totalPages: number;
onPageChange: (page: number) => void;
label?: string;
className?: string;
}) {
const count = Number.isFinite(totalPages)
? Math.max(1, Math.floor(totalPages))
: 1,
current = Number.isFinite(page)
? Math.max(1, Math.min(count, Math.floor(page)))
: 1;
const pages = [
...new Set(
[1, current - 1, current, current + 1, count].filter(
(n) => n >= 1 && n <= count,
),
),
].sort((a, b) => a - b);
return (
);
}
export function Stepper({
steps,
current,
onStepChange,
className = "",
}: {
steps: readonly {
id: string;
label: string;
description?: string;
disabled?: boolean;
}[];
current: string;
onStepChange?: (id: string) => void;
className?: string;
}) {
const index = steps.findIndex((s) => s.id === current);
return (
{steps.map((step, i) => (
-
{String(i + 1).padStart(2, "0")}
{onStepChange ? (
) : (
{step.label}
)}
{step.description && {step.description}}
))}
);
}
export function AnchorNav({
items,
label = "On this page",
className = "",
}: {
items: readonly { id: string; label: string }[];
label?: string;
className?: string;
}) {
const [active, setActive] = useState("");
useEffect(() => {
const nodes = items
.map((item) => document.getElementById(item.id))
.filter((el): el is HTMLElement => !!el);
if (!nodes.length) return;
const observer = new IntersectionObserver(
(entries) => {
const entry = entries.find((e) => e.isIntersecting);
if (entry) setActive(entry.target.id);
},
{ rootMargin: "-10% 0px -65% 0px" },
);
nodes.forEach((node) => observer.observe(node));
return () => observer.disconnect();
}, [items]);
return (
);
}
export function SideNav({
groups,
currentPath,
label = "Sidebar",
className = "",
}: {
groups: readonly { label: string; items: readonly NavigationItem[] }[];
currentPath?: string;
label?: string;
className?: string;
}) {
return (
);
}
export function BottomNav({
items,
currentPath,
label = "Quick navigation",
className = "",
}: {
items: readonly NavigationItem[];
currentPath?: string;
label?: string;
className?: string;
}) {
return (
);
}