refactor: docubook@latest template nextjs-docker
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useLayoutEffect, useRef } from "react";
|
||||
import { ROUTES, EachRoute } from "@/lib/routes";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
@@ -41,8 +41,20 @@ function getIcon(name: string) {
|
||||
export default function ContextPopover({ className }: ContextPopoverProps) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [activeRoute, setActiveRoute] = useState<EachRoute>();
|
||||
const [useDefaultTitle, setUseDefaultTitle] = useState(false);
|
||||
const [triggerWidth, setTriggerWidth] = useState<number | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const contextRoutes = getContextRoutes();
|
||||
const fallbackRoute = ROUTES[0];
|
||||
const displayRoute = useDefaultTitle ? fallbackRoute : activeRoute;
|
||||
|
||||
useEffect(() => {
|
||||
// Mount-only state (used for client-only rendering) and intentionally set after first render.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname.startsWith("/docs")) {
|
||||
@@ -53,7 +65,66 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
if (!pathname.startsWith("/docs") || contextRoutes.length === 0) {
|
||||
useEffect(() => {
|
||||
const hasTitle = Boolean(
|
||||
activeRoute?.context?.title ||
|
||||
activeRoute?.title
|
||||
);
|
||||
|
||||
if (!hasTitle) {
|
||||
const timer = window.setTimeout(() => {
|
||||
setUseDefaultTitle(true);
|
||||
}, 300);
|
||||
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
// Avoid calling setState synchronously inside the effect body.
|
||||
// Using a micro task to reset state avoids the react-hooks/set-state-in-effect lint error.
|
||||
const resetTimer = window.setTimeout(() => {
|
||||
setUseDefaultTitle(false);
|
||||
}, 0);
|
||||
|
||||
return () => window.clearTimeout(resetTimer);
|
||||
}, [activeRoute?.context?.title, activeRoute?.title]);
|
||||
|
||||
// Keep the popover width in sync with the trigger width when the trigger text changes
|
||||
// (e.g. when navigating between docs contexts) and when the window/resizing changes.
|
||||
useLayoutEffect(() => {
|
||||
if (!triggerRef.current) return;
|
||||
|
||||
const updateWidth = () => {
|
||||
if (triggerRef.current) {
|
||||
setTriggerWidth(triggerRef.current.offsetWidth);
|
||||
}
|
||||
};
|
||||
|
||||
// Make sure the width is updated when the trigger text/route changes.
|
||||
updateWidth();
|
||||
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
const observer = new ResizeObserver(() => {
|
||||
updateWidth();
|
||||
});
|
||||
observer.observe(triggerRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
} else {
|
||||
const handleResize = () => {
|
||||
updateWidth();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}
|
||||
}, [displayRoute]);
|
||||
|
||||
if (!mounted || !pathname.startsWith("/docs") || contextRoutes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -61,6 +132,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"w-full cursor-pointer flex items-center justify-between font-semibold text-foreground px-2 py-4 border border-muted",
|
||||
@@ -69,22 +141,29 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{activeRoute?.context?.icon && (
|
||||
{displayRoute?.context?.icon && (
|
||||
<span className="text-primary bg-primary/10 border border-primary dark:border dark:border-accent dark:bg-accent/10 dark:text-accent rounded p-0.5">
|
||||
{getIcon(activeRoute.context.icon)}
|
||||
{getIcon(displayRoute.context.icon)}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate text-sm">
|
||||
{activeRoute?.context?.title || activeRoute?.title || <Skeleton className="h-3.5 w-24" />}
|
||||
{displayRoute?.context?.title ||
|
||||
displayRoute?.title ||
|
||||
(useDefaultTitle
|
||||
? fallbackRoute?.context?.title || fallbackRoute?.title
|
||||
: <Skeleton className="h-3.5 w-24" />)}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="h-4 w-4 text-foreground/50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-64 p-2"
|
||||
className="p-2"
|
||||
align="start"
|
||||
sideOffset={6}
|
||||
style={{
|
||||
width: triggerWidth !== null ? `${triggerWidth}px` : "auto"
|
||||
}}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{contextRoutes.map((route) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { DocSearch } from "@docsearch/react"
|
||||
import "@docsearch/css"
|
||||
import { algoliaConfig } from "@/lib/search/algolia"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -9,7 +10,7 @@ interface AlgoliaSearchProps {
|
||||
}
|
||||
|
||||
export default function AlgoliaSearch({ className }: AlgoliaSearchProps) {
|
||||
const { appId, apiKey, indexName } = algoliaConfig
|
||||
const { appId, apiKey, indexName, askAiAssistantId } = algoliaConfig
|
||||
|
||||
if (!appId || !apiKey || !indexName) {
|
||||
console.error("DocSearch credentials are not set in the environment variables.")
|
||||
@@ -26,7 +27,15 @@ export default function AlgoliaSearch({ className }: AlgoliaSearchProps) {
|
||||
appId={appId}
|
||||
apiKey={apiKey}
|
||||
indexName={indexName}
|
||||
placeholder="Type something to search..."
|
||||
placeholder="Search docs..."
|
||||
askAi={
|
||||
askAiAssistantId
|
||||
? {
|
||||
assistantId: askAiAssistantId,
|
||||
suggestedQuestions: true,
|
||||
} as const
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
@@ -14,20 +13,16 @@ export default function DocsBreadcrumb({ paths }: { paths: string[] }) {
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink>Docs</BreadcrumbLink>
|
||||
<span>Docs</span>
|
||||
</BreadcrumbItem>
|
||||
{paths.map((path, index) => (
|
||||
<Fragment key={`${path}-${index}`}>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
{index < paths.length - 1 ? (
|
||||
<BreadcrumbLink className="a">
|
||||
{toTitleCase(path)}
|
||||
</BreadcrumbLink>
|
||||
<span>{toTitleCase(path)}</span>
|
||||
) : (
|
||||
<BreadcrumbPage className="b">
|
||||
{toTitleCase(path)}
|
||||
</BreadcrumbPage>
|
||||
<BreadcrumbPage>{toTitleCase(path)}</BreadcrumbPage>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
</Fragment>
|
||||
@@ -38,10 +33,23 @@ export default function DocsBreadcrumb({ paths }: { paths: string[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
const acronyms = new Set([
|
||||
"mdx",
|
||||
"api",
|
||||
"pdf",
|
||||
"cli",
|
||||
"ui",
|
||||
"css",
|
||||
"html",
|
||||
"yaml",
|
||||
"json",
|
||||
"ssr",
|
||||
"ssg",
|
||||
]);
|
||||
|
||||
function toTitleCase(input: string): string {
|
||||
const words = input.split("-");
|
||||
const capitalizedWords = words.map(
|
||||
(word) => word.charAt(0).toUpperCase() + word.slice(1)
|
||||
);
|
||||
return capitalizedWords.join(" ");
|
||||
return input
|
||||
.split("-")
|
||||
.map((w) => (acronyms.has(w) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ export default function DocsMenu({ isSheet = false, className = "" }: DocsMenuPr
|
||||
if (!pathname.startsWith("/docs")) return null;
|
||||
|
||||
// Get the current context
|
||||
const currentContext = getCurrentContext(pathname);
|
||||
const isDocsRoot = pathname === "/docs" || pathname === "/docs/";
|
||||
const currentContext = isDocsRoot
|
||||
? ROUTES[0]?.href.replace(/^\/+|\/+$/g, "")
|
||||
: getCurrentContext(pathname);
|
||||
|
||||
// Get the route for the current context
|
||||
const contextRoute = currentContext ? getContextRoute(currentContext) : undefined;
|
||||
@@ -52,9 +55,10 @@ export default function DocsMenu({ isSheet = false, className = "" }: DocsMenuPr
|
||||
<li key={contextRoute.title}>
|
||||
<SubLink
|
||||
{...contextRoute}
|
||||
href={`/docs${contextRoute.href}`}
|
||||
href={contextRoute.href}
|
||||
level={0}
|
||||
isSheet={isSheet}
|
||||
parentHref="/docs"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import GitHubButton from "@/components/Github";
|
||||
import Anchor from "@/components/anchor";
|
||||
import docuConfig from "@/docu.json";
|
||||
|
||||
@@ -36,6 +37,7 @@ export function DocsNavbar() {
|
||||
</Anchor>
|
||||
);
|
||||
})}
|
||||
<GitHubButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,205 +1,188 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, ChevronUp, PanelRight, MoreVertical } from "lucide-react"
|
||||
import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTrigger } from "@/components/ui/sheet"
|
||||
import DocsMenu from "@/components/DocsMenu"
|
||||
import { ModeToggle } from "@/components/ThemeToggle"
|
||||
import { DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
||||
import ContextPopover from "@/components/ContextPopover"
|
||||
import TocObserver from "./TocObserver"
|
||||
import * as React from "react"
|
||||
import { useRef, useMemo } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Button } from "./ui/button"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { useActiveSection } from "@/hooks"
|
||||
import { TocItem } from "@/lib/toc"
|
||||
import Search from "@/components/SearchBox"
|
||||
import { NavMenu } from "@/components/navbar"
|
||||
import { ChevronDown, ChevronUp, PanelRight, MoreVertical, FileText } from "lucide-react";
|
||||
import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTrigger } from "@/components/ui/sheet";
|
||||
import DocsMenu from "@/components/DocsMenu";
|
||||
import { ModeToggle } from "@/components/ThemeToggle";
|
||||
import { DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||
import ContextPopover from "@/components/ContextPopover";
|
||||
import TocObserver from "./TocObserver";
|
||||
import * as React from "react";
|
||||
import { useRef, useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Button } from "./ui/button";
|
||||
import { useActiveSection } from "@/hooks";
|
||||
import { TocItem } from "@/lib/toc";
|
||||
import Search from "@/components/SearchBox";
|
||||
import GitHubButton from "@/components/Github";
|
||||
import { NavMenu } from "@/components/navbar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
interface MobTocProps {
|
||||
tocs: TocItem[]
|
||||
title?: string
|
||||
tocs: TocItem[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const subscribe = () => () => undefined;
|
||||
const getMountedSnapshot = () => true;
|
||||
const getServerSnapshot = () => false;
|
||||
|
||||
const useClickOutside = (ref: React.RefObject<HTMLElement | null>, callback: () => void) => {
|
||||
const handleClick = React.useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
[ref, callback]
|
||||
)
|
||||
const callbackRef = useRef(callback);
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClick)
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClick)
|
||||
}
|
||||
}, [handleClick])
|
||||
}
|
||||
callbackRef.current = callback;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
callbackRef.current();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [ref]);
|
||||
};
|
||||
|
||||
export default function MobToc({ tocs, title }: MobTocProps) {
|
||||
const pathname = usePathname()
|
||||
const [isExpanded, setIsExpanded] = React.useState(false)
|
||||
const tocRef = useRef<HTMLDivElement>(null)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const pathname = usePathname();
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const tocRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Use custom hooks
|
||||
const { activeId, setActiveId } = useActiveSection(tocs)
|
||||
const { activeId, setActiveId } = useActiveSection(tocs);
|
||||
|
||||
// Only show on /docs pages
|
||||
const isDocsPage = useMemo(() => pathname?.startsWith("/docs"), [pathname])
|
||||
const isDocsPage = useMemo(() => pathname?.startsWith("/docs"), [pathname]);
|
||||
|
||||
// Get title from active section if available, otherwise document title
|
||||
const activeSection = useMemo(() => {
|
||||
return tocs.find((toc) => toc.href.slice(1) === activeId)
|
||||
}, [tocs, activeId])
|
||||
return tocs.find((toc) => toc.href.slice(1) === activeId);
|
||||
}, [tocs, activeId]);
|
||||
|
||||
const displayTitle = activeSection?.text || title || "On this page"
|
||||
const displayTitle = activeSection?.text || title || "On this page";
|
||||
|
||||
const [mounted, setMounted] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
const mounted = React.useSyncExternalStore(subscribe, getMountedSnapshot, getServerSnapshot);
|
||||
|
||||
// Toggle expanded state
|
||||
const toggleExpanded = React.useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setIsExpanded((prev) => !prev)
|
||||
}, [])
|
||||
e.stopPropagation();
|
||||
setIsExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// Close TOC when clicking outside
|
||||
useClickOutside(tocRef, () => {
|
||||
if (isExpanded) {
|
||||
setIsExpanded(false)
|
||||
setIsExpanded(false);
|
||||
}
|
||||
})
|
||||
|
||||
// Handle body overflow when TOC is expanded
|
||||
React.useEffect(() => {
|
||||
if (isExpanded) {
|
||||
document.body.style.overflow = "hidden"
|
||||
} else {
|
||||
document.body.style.overflow = ""
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = ""
|
||||
}
|
||||
}, [isExpanded])
|
||||
});
|
||||
|
||||
// Don't render anything if not on docs page
|
||||
if (!isDocsPage || !mounted) return null
|
||||
if (!isDocsPage || !mounted) return null;
|
||||
|
||||
const chevronIcon = isExpanded ? (
|
||||
<ChevronUp className="text-muted-foreground h-4 w-4 shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="text-muted-foreground h-4 w-4 shrink-0" />
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
ref={tocRef}
|
||||
className="sticky top-0 z-50 -mx-4 -mt-4 mb-4 lg:hidden"
|
||||
initial={{ y: -100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: -100, opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
>
|
||||
<div className="bg-background/95 border-muted dark:border-foreground/10 dark:bg-background w-full border-b shadow-sm backdrop-blur-sm">
|
||||
<div className="p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
aria-label="Navigation menu"
|
||||
>
|
||||
<MoreVertical className="text-muted-foreground h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="flex min-w-[160px] flex-col gap-1 p-2"
|
||||
<div ref={tocRef} className="sticky top-0 z-50 -mx-4 -mt-4 mb-4 lg:hidden">
|
||||
<div className="bg-background/95 border-muted dark:border-foreground/10 dark:bg-background w-full border-b shadow-sm backdrop-blur-sm">
|
||||
<div className="p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
aria-label="Navigation menu"
|
||||
>
|
||||
<NavMenu />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-mx-1 h-auto flex-1 justify-between rounded-md px-2 py-2 hover:bg-transparent hover:text-inherit"
|
||||
onClick={toggleExpanded}
|
||||
aria-label={isExpanded ? "Collapse table of contents" : "Expand table of contents"}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium capitalize">{displayTitle}</span>
|
||||
</div>
|
||||
{chevronIcon}
|
||||
</Button>
|
||||
<Search />
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="hidden max-lg:flex">
|
||||
<PanelRight className="text-muted-foreground h-6 w-6 shrink-0" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="flex w-full flex-col gap-4 px-0 lg:w-auto" side="right">
|
||||
<DialogTitle className="sr-only">Navigation Menu</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Main navigation menu with links to different sections
|
||||
</DialogDescription>
|
||||
<SheetHeader>
|
||||
<SheetClose className="px-4" asChild>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mr-8">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
<MoreVertical className="text-muted-foreground h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="flex min-w-40 flex-col gap-1 p-2">
|
||||
<NavMenu />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-mx-1 h-auto min-w-0 flex-1 justify-between rounded-md px-2 py-2 hover:bg-transparent hover:text-inherit"
|
||||
onClick={toggleExpanded}
|
||||
aria-label={isExpanded ? "Collapse table of contents" : "Expand table of contents"}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="line-clamp-1 truncate text-sm font-medium capitalize">
|
||||
{displayTitle}
|
||||
</span>
|
||||
</div>
|
||||
{chevronIcon}
|
||||
</Button>
|
||||
<Search />
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="hidden max-lg:flex">
|
||||
<PanelRight className="text-muted-foreground h-6 w-6 shrink-0" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="flex w-full flex-col gap-4 px-0 lg:w-auto" side="right">
|
||||
<DialogTitle className="sr-only">Navigation Menu</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Main navigation menu with links to different sections
|
||||
</DialogDescription>
|
||||
<SheetHeader>
|
||||
<SheetClose className="px-4" asChild>
|
||||
<div className="flex items-center justify-between">
|
||||
<GitHubButton />
|
||||
<div className="mr-8">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</SheetClose>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-col gap-4 overflow-y-auto">
|
||||
<div className="mx-2 space-y-2 px-5">
|
||||
<ContextPopover />
|
||||
<DocsMenu isSheet />
|
||||
</div>
|
||||
</SheetClose>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-col gap-4 overflow-y-auto">
|
||||
<div className="mx-2 space-y-2 px-5">
|
||||
<ContextPopover />
|
||||
<DocsMenu isSheet />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
ref={contentRef}
|
||||
className="-mx-1 mt-2 max-h-[60vh] overflow-y-auto px-1 pb-2"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
>
|
||||
{tocs?.length ? (
|
||||
<TocObserver data={tocs} activeId={activeId} onActiveIdChange={setActiveId} />
|
||||
) : (
|
||||
<p className="text-muted-foreground py-2 text-sm">No headings</p>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div
|
||||
className="grid transition-[grid-template-rows,opacity] duration-200 ease-in-out"
|
||||
style={{
|
||||
gridTemplateRows: isExpanded ? "1fr" : "0fr",
|
||||
opacity: isExpanded ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div ref={contentRef} className="overflow-hidden">
|
||||
<div className="mt-2 max-h-[60vh] overflow-y-auto px-4 pb-2">
|
||||
{tocs?.length ? (
|
||||
<TocObserver data={tocs} activeId={activeId} onActiveIdChange={setActiveId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center px-2 py-8 text-center">
|
||||
<FileText className="text-muted-foreground/40 mb-3 h-8 w-8" />
|
||||
<p className="text-muted-foreground mb-1 text-sm font-medium">No headings</p>
|
||||
<p className="text-muted-foreground/70 text-xs leading-relaxed">
|
||||
{`This page doesn't have section headings yet.`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from "react";
|
||||
import { searchConfig } from "@/lib/search/config";
|
||||
|
||||
interface SearchContextType {
|
||||
isOpen: boolean;
|
||||
@@ -18,6 +19,9 @@ export function SearchProvider({ children }: { children: React.ReactNode }) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Only add keyboard shortcut for default search, not Algolia (Algolia handles its own)
|
||||
if (searchConfig.type === "algolia") return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect, useMemo, useState, useRef } from "react"
|
||||
import { ArrowUpIcon, ArrowDownIcon, CornerDownLeftIcon, FileTextIcon } from "lucide-react"
|
||||
import Anchor from "./anchor"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { advanceSearch } from "@/lib/search/built-in"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { page_routes } from "@/lib/routes"
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
import { ArrowUpIcon, ArrowDownIcon, CornerDownLeftIcon, FileTextIcon } from "lucide-react";
|
||||
import Anchor from "./anchor";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { advanceSearch } from "@/lib/search/built-in";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { page_routes } from "@/lib/routes";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
@@ -15,49 +15,48 @@ import {
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog"
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
type ContextInfo = {
|
||||
icon: string
|
||||
description: string
|
||||
title?: string
|
||||
}
|
||||
icon: string;
|
||||
description: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
type SearchResult = {
|
||||
title: string
|
||||
href: string
|
||||
noLink?: boolean
|
||||
items?: undefined
|
||||
score?: number
|
||||
context?: ContextInfo
|
||||
}
|
||||
title: string;
|
||||
href: string;
|
||||
noLink?: boolean;
|
||||
items?: undefined;
|
||||
score?: number;
|
||||
context?: ContextInfo;
|
||||
};
|
||||
|
||||
const paddingMap = {
|
||||
1: "pl-2",
|
||||
2: "pl-4",
|
||||
3: "pl-10",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
interface SearchModalProps {
|
||||
isOpen: boolean
|
||||
setIsOpen: (open: boolean) => void
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
||||
const router = useRouter()
|
||||
const [searchedInput, setSearchedInput] = useState("")
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const router = useRouter();
|
||||
const [searchedInput, setSearchedInput] = useState("");
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setSearchedInput("")
|
||||
queueMicrotask(() => setSearchedInput(""));
|
||||
}
|
||||
}, [isOpen])
|
||||
}, [isOpen]);
|
||||
|
||||
const filteredResults = useMemo<SearchResult[]>(() => {
|
||||
const trimmedInput = searchedInput.trim()
|
||||
const trimmedInput = searchedInput.trim();
|
||||
|
||||
if (trimmedInput.length < 3) {
|
||||
return page_routes
|
||||
@@ -68,50 +67,50 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
||||
href: route.href,
|
||||
noLink: route.noLink,
|
||||
context: route.context,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
return advanceSearch(trimmedInput) as unknown as SearchResult[]
|
||||
}, [searchedInput])
|
||||
return advanceSearch(trimmedInput) as unknown as SearchResult[];
|
||||
}, [searchedInput]);
|
||||
|
||||
// useEffect(() => {
|
||||
// setSelectedIndex(0);
|
||||
// }, [filteredResults]);
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => setSelectedIndex(0));
|
||||
}, [filteredResults]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleNavigation = (event: KeyboardEvent) => {
|
||||
if (!isOpen || filteredResults.length === 0) return
|
||||
if (!isOpen || filteredResults.length === 0) return;
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
setSelectedIndex((prev) => (prev + 1) % filteredResults.length)
|
||||
event.preventDefault();
|
||||
setSelectedIndex((prev) => (prev + 1) % filteredResults.length);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault()
|
||||
setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length)
|
||||
event.preventDefault();
|
||||
setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length);
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
const selectedItem = filteredResults[selectedIndex]
|
||||
event.preventDefault();
|
||||
const selectedItem = filteredResults[selectedIndex];
|
||||
if (selectedItem) {
|
||||
router.push(`/docs${selectedItem.href}`)
|
||||
setIsOpen(false)
|
||||
router.push(`/docs${selectedItem.href}`);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleNavigation)
|
||||
return () => window.removeEventListener("keydown", handleNavigation)
|
||||
}, [isOpen, filteredResults, selectedIndex, router, setIsOpen])
|
||||
window.addEventListener("keydown", handleNavigation);
|
||||
return () => window.removeEventListener("keydown", handleNavigation);
|
||||
}, [isOpen, filteredResults, selectedIndex, router, setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemRefs.current[selectedIndex]) {
|
||||
itemRefs.current[selectedIndex]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [selectedIndex])
|
||||
}, [selectedIndex]);
|
||||
|
||||
return (
|
||||
<DialogContent className="rounded-md! top-[45%] max-w-[650px] p-0 sm:top-[38%]">
|
||||
<DialogContent className="top-[45%] max-w-[650px] rounded-md! p-0 sm:top-[38%]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="sr-only">Search Documentation</DialogTitle>
|
||||
<DialogDescription className="sr-only">Search through the documentation</DialogDescription>
|
||||
@@ -120,8 +119,8 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
||||
<input
|
||||
value={searchedInput}
|
||||
onChange={(e) => {
|
||||
setSearchedInput(e.target.value)
|
||||
setSelectedIndex(0)
|
||||
setSearchedInput(e.target.value);
|
||||
setSelectedIndex(0);
|
||||
}}
|
||||
placeholder="Type something to search..."
|
||||
autoFocus
|
||||
@@ -137,15 +136,15 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
||||
<ScrollArea className="max-h-[400px] overflow-y-auto">
|
||||
<div className="flex flex-col items-start overflow-y-auto px-1 pb-4 sm:px-2">
|
||||
{filteredResults.map((item, index) => {
|
||||
const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap
|
||||
const paddingClass = paddingMap[level] || "pl-2"
|
||||
const isActive = index === selectedIndex
|
||||
const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap;
|
||||
const paddingClass = paddingMap[level] || "pl-2";
|
||||
const isActive = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<DialogClose key={item.href} asChild>
|
||||
<Anchor
|
||||
ref={(el) => {
|
||||
itemRefs.current[index] = el as HTMLDivElement | null
|
||||
itemRefs.current[index] = el as HTMLDivElement | null;
|
||||
}}
|
||||
className={cn(
|
||||
"dark:hover:bg-accent/15 hover:bg-accent/10 flex w-full items-center gap-2.5 rounded-sm px-3 text-sm",
|
||||
@@ -174,7 +173,7 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
||||
</div>
|
||||
</Anchor>
|
||||
</DialogClose>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
@@ -196,5 +195,5 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import docuData from "@/docu.json";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
// Define types for docu.json
|
||||
interface SponsorItem {
|
||||
url: string;
|
||||
image: string;
|
||||
@@ -10,116 +9,31 @@ interface SponsorItem {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface NavbarConfig {
|
||||
title?: string;
|
||||
logo?: {
|
||||
light?: string;
|
||||
dark?: string;
|
||||
};
|
||||
links?: Array<{
|
||||
title: string;
|
||||
href: string;
|
||||
external?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface FooterConfig {
|
||||
text?: string;
|
||||
links?: Array<{
|
||||
title: string;
|
||||
href: string;
|
||||
external?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface MetaConfig {
|
||||
title?: string;
|
||||
description?: string;
|
||||
favicon?: string;
|
||||
socialBanner?: string;
|
||||
}
|
||||
|
||||
interface RepositoryConfig {
|
||||
url: string;
|
||||
editUrl?: string;
|
||||
branch?: string;
|
||||
directory?: string;
|
||||
}
|
||||
|
||||
interface RouteItem {
|
||||
title: string;
|
||||
href: string;
|
||||
noLink?: boolean;
|
||||
context?: {
|
||||
icon: string;
|
||||
description: string;
|
||||
title: string;
|
||||
};
|
||||
items?: RouteItem[];
|
||||
}
|
||||
|
||||
interface RouteConfig {
|
||||
title: string;
|
||||
href: string;
|
||||
noLink?: boolean;
|
||||
context?: {
|
||||
icon: string;
|
||||
description: string;
|
||||
title: string;
|
||||
};
|
||||
items?: RouteItem[];
|
||||
}
|
||||
|
||||
interface DocuConfig {
|
||||
sponsor?: {
|
||||
title?: string;
|
||||
item?: SponsorItem;
|
||||
};
|
||||
navbar: NavbarConfig;
|
||||
footer: FooterConfig;
|
||||
meta: MetaConfig;
|
||||
repository: RepositoryConfig;
|
||||
routes: RouteConfig[];
|
||||
}
|
||||
|
||||
// Type assertion for docu.json
|
||||
const docuConfig = docuData as DocuConfig;
|
||||
const docuConfig = docuData as { sponsor?: { title?: string; item?: SponsorItem } };
|
||||
|
||||
export function Sponsor() {
|
||||
// Safely get sponsor data with optional chaining and default values
|
||||
const sponsor = docuConfig?.sponsor || {};
|
||||
const item = sponsor?.item;
|
||||
|
||||
// Return null if required fields are missing
|
||||
if (!item?.url || !item?.image || !item?.title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
{sponsor?.title && (
|
||||
<h2 className="mb-4 text-sm font-medium">{sponsor.title}</h2>
|
||||
)}
|
||||
{sponsor?.title && <h2 className="mb-4 text-sm font-medium">{sponsor.title}</h2>}
|
||||
<Link
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex flex-col justify-center gap-2 p-4 border rounded-lg hover:shadow transition-shadow"
|
||||
className="flex flex-col justify-center gap-2 rounded-lg border p-4 transition-shadow hover:shadow"
|
||||
>
|
||||
<div className="relative w-8 h-8 shrink-0">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-contain"
|
||||
sizes="32px"
|
||||
/>
|
||||
<div className="relative h-8 w-8 shrink-0">
|
||||
<Image src={item.image} alt={item.title} fill className="object-contain" sizes="32px" />
|
||||
</div>
|
||||
<div className="text-center sm:text-left">
|
||||
<h3 className="text-sm font-medium">{item.title}</h3>
|
||||
{item.description && (
|
||||
<p className="text-muted-foreground text-sm">{item.description}</p>
|
||||
)}
|
||||
{item.description && <p className="text-muted-foreground text-sm">{item.description}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
interface Props extends ThemeProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children, ...props }: Props) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
|
||||
@@ -7,35 +7,27 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
const mounted = React.useSyncExternalStore(
|
||||
() => () => {},
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
|
||||
// Untuk menghindari hydration mismatch
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Jika belum mounted, jangan render apapun untuk menghindari mismatch
|
||||
// If not mounted, do not render anything to avoid mismatch
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 rounded-full border border-border bg-background/50 p-0.5">
|
||||
<div className="rounded-full p-0 w-1 h-1" />
|
||||
<div className="rounded-full p-0 w-1 h-1" />
|
||||
<div className="border-border bg-background/50 flex items-center gap-1 rounded-full border p-0.5">
|
||||
<div className="h-1 w-1 rounded-full p-0" />
|
||||
<div className="h-1 w-1 rounded-full p-0" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Tentukan theme yang aktif: gunakan resolvedTheme untuk menampilkan ikon yang sesuai
|
||||
// jika theme === "system", resolvedTheme akan menjadi "light" atau "dark" sesuai device
|
||||
const activeTheme = theme === "system" || !theme ? resolvedTheme : theme;
|
||||
|
||||
const handleToggle = () => {
|
||||
// Toggle antara light dan dark
|
||||
// Jika sekarang light, ganti ke dark, dan sebaliknya
|
||||
if (activeTheme === "light") {
|
||||
setTheme("dark");
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
const handleToggle = (value: string) => {
|
||||
if (!value) return;
|
||||
setTheme(value);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -43,29 +35,31 @@ export function ModeToggle() {
|
||||
type="single"
|
||||
value={activeTheme}
|
||||
onValueChange={handleToggle}
|
||||
className="flex items-center gap-1 rounded-full border border-border bg-background/50 p-0.5 transition-all"
|
||||
className="border-border bg-background/50 flex items-center gap-1 rounded-full border p-0.5 transition-all"
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="light"
|
||||
size="xs"
|
||||
aria-label="Light Mode"
|
||||
className={`rounded-full cursor-pointer p-0.5 transition-all ${activeTheme === "light"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-transparent hover:bg-muted/50"
|
||||
}`}
|
||||
className={`cursor-pointer rounded-full p-0.5 transition-all ${
|
||||
activeTheme === "light"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted/50 bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<Sun className="h-0.5 w-0.5" />
|
||||
<Sun className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="dark"
|
||||
size="xs"
|
||||
aria-label="Dark Mode"
|
||||
className={`rounded-full cursor-pointer p-0.5 transition-all ${activeTheme === "dark"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-transparent hover:bg-muted/50"
|
||||
}`}
|
||||
className={`cursor-pointer rounded-full p-0.5 transition-all ${
|
||||
activeTheme === "dark"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted/50 bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<Moon className="h-0.5 w-0.5" />
|
||||
<Moon className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx"
|
||||
import Link from "next/link"
|
||||
import { useState, useRef, useEffect, useCallback } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { ScrollToTop } from "./ScrollToTop"
|
||||
import { TocItem } from "@/lib/toc"
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRef, useCallback } from "react";
|
||||
import { ScrollToTop } from "./ScrollToTop";
|
||||
import { TocItem } from "@/lib/toc";
|
||||
|
||||
interface TocObserverProps {
|
||||
data: TocItem[]
|
||||
activeId?: string | null
|
||||
onActiveIdChange?: (id: string | null) => void
|
||||
data: TocItem[];
|
||||
activeId?: string | null;
|
||||
onActiveIdChange?: (id: string | null) => void;
|
||||
}
|
||||
|
||||
export default function TocObserver({
|
||||
@@ -18,180 +17,95 @@ export default function TocObserver({
|
||||
activeId: externalActiveId,
|
||||
onActiveIdChange,
|
||||
}: TocObserverProps) {
|
||||
const itemRefs = useRef<Map<string, HTMLAnchorElement>>(new Map())
|
||||
const itemRefs = useRef<Map<string, HTMLAnchorElement>>(new Map());
|
||||
|
||||
const activeId = externalActiveId ?? null
|
||||
const activeId = externalActiveId ?? null;
|
||||
|
||||
const handleLinkClick = useCallback(
|
||||
(id: string) => {
|
||||
onActiveIdChange?.(id)
|
||||
onActiveIdChange?.(id);
|
||||
},
|
||||
[onActiveIdChange]
|
||||
)
|
||||
|
||||
// Function to check if an item has children
|
||||
const hasChildren = (currentId: string, currentLevel: number) => {
|
||||
const currentIndex = data.findIndex((item) => item.href.slice(1) === currentId)
|
||||
if (currentIndex === -1 || currentIndex === data.length - 1) return false
|
||||
|
||||
const nextItem = data[currentIndex + 1]
|
||||
return nextItem.level > currentLevel
|
||||
}
|
||||
|
||||
// Calculate scroll progress for the active section
|
||||
const [scrollProgress, setScrollProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!activeId) return
|
||||
|
||||
const activeElement = document.getElementById(activeId)
|
||||
if (!activeElement) return
|
||||
|
||||
const rect = activeElement.getBoundingClientRect()
|
||||
const windowHeight = window.innerHeight
|
||||
const elementTop = rect.top
|
||||
const elementHeight = rect.height
|
||||
|
||||
// Calculate how much of the element is visible
|
||||
let progress = 0
|
||||
if (elementTop < windowHeight) {
|
||||
progress = Math.min(1, (windowHeight - elementTop) / (windowHeight + elementHeight))
|
||||
}
|
||||
|
||||
setScrollProgress(progress)
|
||||
}
|
||||
|
||||
const container = document.getElementById("scroll-container") || window
|
||||
|
||||
container.addEventListener("scroll", handleScroll, { passive: true })
|
||||
|
||||
// Initial calculation
|
||||
handleScroll()
|
||||
|
||||
return () => container.removeEventListener("scroll", handleScroll)
|
||||
}, [activeId])
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="text-foreground/70 hover:text-foreground relative text-sm transition-colors">
|
||||
{/* Single vertical line on the left */}
|
||||
<div className="bg-border/40 dark:bg-border/30 absolute top-0 left-0 h-full w-px" />
|
||||
|
||||
<div className="flex flex-col gap-0">
|
||||
{data.map(({ href, level, text }, index) => {
|
||||
const id = href.slice(1)
|
||||
const isActive = activeId === id
|
||||
const indent = level > 1 ? (level - 1) * 20 : 0
|
||||
// Prefix with underscore to indicate intentionally unused
|
||||
const _isParent = hasChildren(id, level)
|
||||
const _isLastInLevel = index === data.length - 1 || data[index + 1].level <= level
|
||||
{data.map(({ href, level, text }) => {
|
||||
const id = href.slice(1);
|
||||
const isActive = activeId === id;
|
||||
// Calculate padding based on level for indentation
|
||||
const levelPadding = (level - 2) * 16; // 0px for level 2, 16px for level 3, 32px for level 4, etc
|
||||
|
||||
return (
|
||||
<div key={href} className="relative">
|
||||
{/* Simple L-shaped connector */}
|
||||
{level > 1 && (
|
||||
<div
|
||||
className={clsx("absolute top-0 h-full w-6", {
|
||||
"left-[6px]": indent === 20, // Level 2
|
||||
"left-[22px]": indent === 40, // Level 3
|
||||
"left-[38px]": indent === 60, // Level 4
|
||||
})}
|
||||
>
|
||||
{/* Vertical line */}
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute left-0 top-0 h-full w-px",
|
||||
isActive
|
||||
? "bg-primary/20 dark:bg-primary/30"
|
||||
: "bg-border/50 dark:bg-border/50"
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
className="bg-primary absolute left-0 top-0 h-full w-full origin-top"
|
||||
initial={{ scaleY: 0 }}
|
||||
animate={{ scaleY: scrollProgress }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Horizontal line */}
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute left-0 top-1/2 h-px w-6",
|
||||
isActive
|
||||
? "bg-primary/20 dark:bg-primary/30"
|
||||
: "bg-border/50 dark:bg-border/50"
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
className="bg-primary dark:bg-accent absolute left-0 top-0 h-full w-full origin-left"
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: scrollProgress }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
key={href}
|
||||
className={clsx(
|
||||
"relative flex items-center transition-all duration-200",
|
||||
isActive ? "bg-primary/5 dark:bg-primary/10" : ""
|
||||
)}
|
||||
>
|
||||
{/* Horizontal line connected to vertical line + Dot */}
|
||||
<div
|
||||
className={clsx(
|
||||
"flex shrink-0 items-center px-1 pt-2 pb-2 transition-all duration-200",
|
||||
isActive ? "border-primary -ml-px border-l-[3px]" : ""
|
||||
)}
|
||||
>
|
||||
{/* Horizontal line from vertical line to dot */}
|
||||
<div
|
||||
className={clsx(
|
||||
"h-px transition-colors duration-200",
|
||||
isActive
|
||||
? "bg-primary dark:bg-primary w-3"
|
||||
: "bg-border/40 dark:bg-border/30 w-2"
|
||||
)}
|
||||
/>
|
||||
{/* Dot */}
|
||||
<div
|
||||
className={clsx(
|
||||
"h-1.5 w-1.5 shrink-0 rounded-full transition-colors duration-300",
|
||||
{
|
||||
"bg-primary dark:bg-primary": isActive,
|
||||
"bg-border/50 dark:bg-border/40": !isActive,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Text link with indentation padding */}
|
||||
<Link
|
||||
href={href}
|
||||
onClick={() => handleLinkClick(id)}
|
||||
className={clsx("relative flex items-center py-2 transition-colors", {
|
||||
aria-current={isActive ? "location" : undefined}
|
||||
className={clsx("flex flex-1 items-center py-2 transition-all duration-200", {
|
||||
"text-primary dark:text-primary font-medium": isActive,
|
||||
"text-muted-foreground hover:text-foreground dark:hover:text-foreground/90":
|
||||
!isActive,
|
||||
})}
|
||||
style={{
|
||||
paddingLeft: `${indent}px`,
|
||||
marginLeft: level > 1 ? "12px" : "0",
|
||||
}}
|
||||
ref={(el) => {
|
||||
const map = itemRefs.current
|
||||
style={{ paddingLeft: `${levelPadding + 6}px` }}
|
||||
ref={(el: HTMLAnchorElement | null) => {
|
||||
const map = itemRefs.current;
|
||||
if (el) {
|
||||
map.set(id, el)
|
||||
map.set(id, el);
|
||||
} else {
|
||||
map.delete(id)
|
||||
map.delete(id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Circle indicator */}
|
||||
<div className="relative flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
<div
|
||||
className={clsx(
|
||||
"relative z-10 h-1.5 w-1.5 rounded-full transition-all duration-300",
|
||||
{
|
||||
"bg-primary dark:bg-primary/90 scale-100": isActive,
|
||||
"bg-muted-foreground/30 dark:bg-muted-foreground/30 group-hover:bg-primary/50 dark:group-hover:bg-primary/50 scale-75 group-hover:scale-100":
|
||||
!isActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
className="bg-primary/20 dark:bg-primary/30 absolute inset-0 rounded-full"
|
||||
initial={{ scale: 1 }}
|
||||
animate={{ scale: 1.8 }}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="truncate text-sm">{text}</span>
|
||||
<span className="line-clamp-2 text-sm break-words">{text}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* Add scroll to top link at the bottom of TOC */}
|
||||
<ScrollToTop className="mt-6" />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,42 +14,38 @@ type AnchorProps = LinkProps & {
|
||||
} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps>;
|
||||
|
||||
const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(
|
||||
({
|
||||
absolute = false,
|
||||
className = "",
|
||||
activeClassName = "",
|
||||
disabled = false,
|
||||
children,
|
||||
href,
|
||||
...props
|
||||
}, ref) => {
|
||||
(
|
||||
{
|
||||
absolute = false,
|
||||
className = "",
|
||||
activeClassName = "",
|
||||
disabled = false,
|
||||
children,
|
||||
href,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const path = usePathname();
|
||||
const hrefStr = href?.toString() || '';
|
||||
const hrefStr = href?.toString() || "";
|
||||
|
||||
// Check if URL is external
|
||||
const isExternal = /^(https?:\/\/|\/\/)/.test(hrefStr);
|
||||
|
||||
// Check if current path matches the link
|
||||
const isActive = absolute
|
||||
? hrefStr.split("/")[1] === path?.split("/")[1]
|
||||
: path === hrefStr;
|
||||
const isActive = absolute ? hrefStr.split("/")[1] === path?.split("/")[1] : path === hrefStr;
|
||||
|
||||
// Apply active class only for internal links
|
||||
const linkClassName = cn(
|
||||
'transition-colors hover:text-primary',
|
||||
"transition-colors hover:text-primary",
|
||||
className,
|
||||
!isExternal && isActive && activeClassName
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<span className={cn(linkClassName, "cursor-not-allowed opacity-50")}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
return <span className={cn(linkClassName, "cursor-not-allowed opacity-50")}>{children}</span>;
|
||||
}
|
||||
|
||||
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a
|
||||
@@ -65,12 +61,12 @@ const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
href={hrefStr}
|
||||
className={linkClassName}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import { ModeToggle } from "@/components/ThemeToggle";
|
||||
import docuData from "@/docu.json";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { getSocialIconByName } from "@/lib/icon";
|
||||
|
||||
// Define types for docu.json
|
||||
interface SocialItem {
|
||||
name: string;
|
||||
url: string;
|
||||
iconName: string;
|
||||
}
|
||||
|
||||
interface FooterConfig {
|
||||
@@ -48,7 +47,7 @@ export function Footer({ id }: FooterProps) {
|
||||
export function FooterButtons() {
|
||||
const footer = docuConfig?.footer;
|
||||
|
||||
// Jangan render apapun jika tidak ada data sosial
|
||||
// Don't render anything if there is no social data
|
||||
if (!footer || !Array.isArray(footer.social) || footer.social.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -56,9 +55,7 @@ export function FooterButtons() {
|
||||
return (
|
||||
<>
|
||||
{footer.social.map((item) => {
|
||||
const IconComponent =
|
||||
(LucideIcons[item.iconName as keyof typeof LucideIcons] ??
|
||||
LucideIcons["Globe"]) as React.FC<{ className?: string }>;
|
||||
const IconComponent = getSocialIconByName(item.name);
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -9,6 +9,7 @@ import DocsMenu from "@/components/DocsMenu"
|
||||
import { ModeToggle } from "@/components/ThemeToggle"
|
||||
import ContextPopover from "@/components/ContextPopover"
|
||||
import Search from "@/components/SearchBox"
|
||||
import GitHubButton from "@/components/Github"
|
||||
|
||||
export function Leftbar() {
|
||||
return (
|
||||
@@ -54,7 +55,10 @@ export function SheetLeftbar() {
|
||||
<SheetHeader>
|
||||
<SheetClose className="px-4" asChild>
|
||||
<div className="flex items-center justify-between">
|
||||
<ModeToggle />
|
||||
<GitHubButton />
|
||||
<div className="mr-8">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</SheetClose>
|
||||
</SheetHeader>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createContext, useState, useId } from "react"
|
||||
|
||||
type AccordionGroupContextType = {
|
||||
inGroup: boolean
|
||||
groupId: string
|
||||
openTitle: string | null
|
||||
setOpenTitle: (title: string | null) => void
|
||||
}
|
||||
|
||||
export const AccordionGroupContext = createContext<AccordionGroupContextType | null>(null)
|
||||
|
||||
export function AccordionGroupProvider({ children }: { children: React.ReactNode }) {
|
||||
const [openTitle, setOpenTitle] = useState<string | null>(null)
|
||||
const groupId = useId()
|
||||
|
||||
return (
|
||||
<AccordionGroupContext.Provider value={{ inGroup: true, groupId, openTitle, setOpenTitle }}>
|
||||
{children}
|
||||
</AccordionGroupContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import React, { ReactNode } from "react"
|
||||
import clsx from "clsx"
|
||||
import { AccordionGroupProvider } from "@/components/markdown/AccordionContext"
|
||||
|
||||
interface AccordionGroupProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AccordionGroup: React.FC<AccordionGroupProps> = ({ children, className }) => {
|
||||
return (
|
||||
<AccordionGroupProvider>
|
||||
<div className={clsx("overflow-hidden rounded-lg border", className)}>{children}</div>
|
||||
</AccordionGroupProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccordionGroup
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { ReactNode, useContext, useState } from "react"
|
||||
import { ChevronRight } from "lucide-react"
|
||||
import * as Icons from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { AccordionGroupContext } from "@/components/markdown/AccordionContext"
|
||||
|
||||
type AccordionProps = {
|
||||
title: string
|
||||
children?: ReactNode
|
||||
icon?: keyof typeof Icons
|
||||
}
|
||||
|
||||
const Accordion: React.FC<AccordionProps> = ({ title, children, icon }: AccordionProps) => {
|
||||
const groupContext = useContext(AccordionGroupContext)
|
||||
const isInGroup = groupContext?.inGroup === true
|
||||
const groupOpen = groupContext?.openTitle === title
|
||||
const setGroupOpen = groupContext?.setOpenTitle
|
||||
const [localOpen, setLocalOpen] = useState(false)
|
||||
|
||||
const isOpen = isInGroup ? groupOpen : localOpen
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isInGroup && setGroupOpen) {
|
||||
setGroupOpen(groupOpen ? null : title)
|
||||
} else {
|
||||
setLocalOpen(!localOpen)
|
||||
}
|
||||
}
|
||||
|
||||
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
!isInGroup && "rounded-lg border shadow-sm",
|
||||
isInGroup && "border-border border-b last:border-b-0"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
className="bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70 flex w-full cursor-pointer items-center gap-2 px-4 py-3 text-start transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200",
|
||||
isOpen && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
{Icon && <Icon className="text-foreground h-4 w-4 shrink-0" />}
|
||||
<h3 className="text-foreground m-0! text-base font-medium">{title}</h3>
|
||||
</button>
|
||||
|
||||
{isOpen && <div className="dark:bg-muted/10 bg-muted/15 px-4 py-3">{children}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Accordion
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from "react";
|
||||
import * as Icons from "lucide-react";
|
||||
import Link from "next/link";
|
||||
type ButtonProps = {
|
||||
icon?: keyof typeof Icons;
|
||||
text?: string;
|
||||
href: string;
|
||||
target?: "_blank" | "_self" | "_parent" | "_top";
|
||||
size?: "sm" | "md" | "lg";
|
||||
variation?: "primary" | "accent" | "outline";
|
||||
};
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
icon,
|
||||
text,
|
||||
href,
|
||||
target,
|
||||
size = "md",
|
||||
variation = "primary",
|
||||
}) => {
|
||||
const baseStyles = "inline-flex items-center justify-center rounded font-medium focus:outline-none transition no-underline";
|
||||
|
||||
const sizeStyles = {
|
||||
sm: "px-3 py-1 my-6 text-sm",
|
||||
md: "px-4 py-2 my-6 text-base",
|
||||
lg: "px-5 py-3 my-6 text-lg",
|
||||
};
|
||||
|
||||
const variationStyles = {
|
||||
primary: "bg-primary text-white hover:bg-primary/90",
|
||||
accent: "bg-accent text-white hover:bg-accent/90",
|
||||
outline: "border border-accent text-accent hover:bg-accent/10",
|
||||
};
|
||||
|
||||
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null; // Tipe eksplisit sebagai React.FC
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
target={target}
|
||||
rel={target === "_blank" ? "noopener noreferrer" : undefined}
|
||||
className={`${baseStyles} ${sizeStyles[size]} ${variationStyles[variation]}`}
|
||||
>
|
||||
{text && <span>{text}</span>}
|
||||
{Icon && <Icon className="mr-2 h-5 w-5" />}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface CardGroupProps {
|
||||
children: ReactNode;
|
||||
cols?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className }) => {
|
||||
const cardsArray = React.Children.toArray(children);
|
||||
|
||||
// Static grid column classes for Tailwind v4 compatibility
|
||||
const gridColsClass = {
|
||||
1: "grid-cols-1",
|
||||
2: "grid-cols-1 sm:grid-cols-2",
|
||||
3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
||||
4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
|
||||
}[cols] || "grid-cols-1 sm:grid-cols-2";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"grid gap-4 text-foreground",
|
||||
gridColsClass,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{cardsArray.map((card, index) => (
|
||||
<div key={index}>{card}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardGroup;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import * as Icons from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import clsx from "clsx";
|
||||
|
||||
type IconName = keyof typeof Icons;
|
||||
|
||||
interface CardProps {
|
||||
title: string;
|
||||
icon?: IconName;
|
||||
href?: string;
|
||||
horizontal?: boolean;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Card: React.FC<CardProps> = ({ title, icon, href, horizontal, children, className }) => {
|
||||
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null;
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={clsx(
|
||||
"border rounded-lg shadow-sm p-4 transition-all duration-200",
|
||||
"bg-card text-card-foreground border-border",
|
||||
"hover:bg-accent/5 hover:border-accent/30",
|
||||
"flex gap-2",
|
||||
horizontal ? "flex-row items-start gap-1" : "flex-col space-y-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon className={clsx("w-5 h-5 text-primary shrink-0", horizontal && "mt-0.5")} />}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base font-semibold text-foreground leading-6">{title}</div>
|
||||
<div className="text-sm text-muted-foreground -mt-3">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return href ? <Link className="no-underline block" href={href}>{content}</Link> : content;
|
||||
};
|
||||
|
||||
export default Card;
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Copy({ content }: { content: string }) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
async function handleCopy() {
|
||||
await navigator.clipboard.writeText(content);
|
||||
setIsCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="border cursor-copy"
|
||||
size="xs"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CheckIcon className="w-3 h-3" />
|
||||
) : (
|
||||
<CopyIcon className="w-3 h-3" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, ReactNode, Children, isValidElement, cloneElement } from 'react';
|
||||
import { ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen } from 'lucide-react';
|
||||
|
||||
interface FileProps {
|
||||
name: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const FileComponent = ({ name }: FileProps) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const fileExtension = name.split('.').pop()?.toUpperCase();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-2 py-1.5 pl-7 pr-3 text-sm rounded-md
|
||||
transition-colors duration-150 cursor-default select-none
|
||||
${isHovered ? 'bg-accent/10' : 'hover:bg-muted/50'}
|
||||
`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<FileIcon className={`
|
||||
h-3.5 w-3.5 shrink-0 transition-colors
|
||||
${isHovered ? 'text-accent' : 'text-muted-foreground'}
|
||||
`} />
|
||||
<span className="font-mono text-sm text-foreground truncate">{name}</span>
|
||||
{isHovered && fileExtension && (
|
||||
<span className="ml-auto text-xs text-muted-foreground/80">
|
||||
{fileExtension}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FolderComponent = ({ name, children }: FileProps) => {
|
||||
const [isOpen, setIsOpen] = useState(true); // Set to true by default
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const hasChildren = React.Children.count(children) > 0;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-2 py-1.5 pl-4 pr-3 rounded-md
|
||||
transition-colors duration-150 select-none
|
||||
${isHovered ? 'bg-muted/60' : ''}
|
||||
${isOpen ? 'text-foreground' : 'text-foreground/80'}
|
||||
${hasChildren ? 'cursor-pointer' : 'cursor-default'}
|
||||
`}
|
||||
onClick={() => hasChildren && setIsOpen(!isOpen)}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
tabIndex={-1}
|
||||
onKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{hasChildren ? (
|
||||
<ChevronRight
|
||||
className={`
|
||||
h-3.5 w-3.5 shrink-0 transition-transform duration-200
|
||||
${isOpen ? 'rotate-90' : ''}
|
||||
${isHovered ? 'text-foreground/70' : 'text-muted-foreground'}
|
||||
`}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-3.5" />
|
||||
)}
|
||||
{isOpen ? (
|
||||
<FolderOpen className={`
|
||||
h-4 w-4 shrink-0 transition-colors
|
||||
${isHovered ? 'text-accent' : 'text-muted-foreground'}
|
||||
`} />
|
||||
) : (
|
||||
<FolderIcon className={`
|
||||
h-4 w-4 shrink-0 transition-colors
|
||||
${isHovered ? 'text-accent/80' : 'text-muted-foreground/80'}
|
||||
`} />
|
||||
)}
|
||||
<span className={`
|
||||
font-medium transition-colors duration-150
|
||||
${isHovered ? 'text-accent' : ''}
|
||||
`}>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
{isOpen && hasChildren && (
|
||||
<div className="ml-5 border-l-2 border-muted/50 pl-2">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Files = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<div
|
||||
className="
|
||||
rounded-xl border border-muted/20
|
||||
bg-card/20 backdrop-blur-sm
|
||||
shadow-sm overflow-hidden
|
||||
transition-all duration-200
|
||||
hover:shadow-md hover:border-muted/60
|
||||
"
|
||||
onKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="p-2">
|
||||
{Children.map(children, (child, index) => {
|
||||
if (isValidElement(child)) {
|
||||
return cloneElement(child, { key: index });
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Folder = ({ name, children }: FileProps) => {
|
||||
return <FolderComponent name={name}>{children}</FolderComponent>;
|
||||
};
|
||||
|
||||
export const File = ({ name }: FileProps) => {
|
||||
return <FileComponent name={name} />;
|
||||
};
|
||||
|
||||
// MDX Components
|
||||
export const FileTreeMdx = {
|
||||
Files,
|
||||
File,
|
||||
Folder,
|
||||
};
|
||||
|
||||
export default FileTreeMdx;
|
||||
@@ -1,129 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ComponentProps, useState, useEffect } from "react";
|
||||
import NextImage from "next/image";
|
||||
import { createPortal } from "react-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { X, ZoomIn } from "lucide-react";
|
||||
|
||||
type Height = ComponentProps<typeof NextImage>["height"];
|
||||
type Width = ComponentProps<typeof NextImage>["width"];
|
||||
|
||||
type ImageProps = Omit<ComponentProps<"img">, "src"> & {
|
||||
src?: ComponentProps<typeof NextImage>["src"];
|
||||
};
|
||||
|
||||
export default function Image({
|
||||
src,
|
||||
alt = "alt",
|
||||
width = 800,
|
||||
height = 350,
|
||||
...props
|
||||
}: ImageProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Lock scroll when open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
// Check for Escape key
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setIsOpen(false);
|
||||
};
|
||||
window.addEventListener("keydown", handleEsc);
|
||||
return () => {
|
||||
document.body.style.overflow = "auto";
|
||||
window.removeEventListener("keydown", handleEsc);
|
||||
};
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!src) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="relative group cursor-zoom-in my-6 w-full flex justify-center rounded-lg"
|
||||
onClick={() => setIsOpen(true)}
|
||||
aria-label="Zoom image"
|
||||
>
|
||||
<span className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors z-10 flex items-center justify-center opacity-0 group-hover:opacity-100 rounded-lg">
|
||||
<ZoomIn className="w-8 h-8 text-white drop-shadow-md" />
|
||||
</span>
|
||||
<NextImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width as Width}
|
||||
height={height as Height}
|
||||
quality={85}
|
||||
className="w-full h-auto rounded-lg transition-transform duration-300 group-hover:scale-[1.01]"
|
||||
{...props}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-99999 flex items-center justify-center bg-black/90 backdrop-blur-md p-4 md:p-10 cursor-zoom-out"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
className="absolute top-4 right-4 z-50 p-2 text-white/70 hover:text-white bg-black/20 hover:bg-white/10 rounded-full transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{/* Image Container */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
className="relative max-w-7xl w-full h-full flex items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="relative w-full h-full flex items-center justify-center" onClick={() => setIsOpen(false)}>
|
||||
<NextImage
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="object-contain max-h-[90vh] w-auto h-auto rounded-md shadow-2xl"
|
||||
quality={95}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Caption */}
|
||||
{alt && alt !== "alt" && (
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
className="absolute bottom-6 left-1/2 -translate-x-1/2 bg-black/60 text-white px-4 py-2 rounded-full text-sm font-medium backdrop-blur-md border border-white/10"
|
||||
>
|
||||
{alt}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</motion.div>
|
||||
</Portal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Portal = ({ children }: { children: React.ReactNode }) => {
|
||||
if (typeof window === "undefined") return null;
|
||||
return createPortal(children, document.body);
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
// Map of special keys to their Mac symbols
|
||||
const macKeyMap: Record<string, string> = {
|
||||
command: '⌘',
|
||||
cmd: '⌘',
|
||||
option: '⌥',
|
||||
alt: '⌥',
|
||||
shift: '⇧',
|
||||
ctrl: '⌃',
|
||||
control: '⌃',
|
||||
tab: '⇥',
|
||||
caps: '⇪',
|
||||
enter: '⏎',
|
||||
return: '⏎',
|
||||
delete: '⌫',
|
||||
escape: '⎋',
|
||||
esc: '⎋',
|
||||
up: '↑',
|
||||
down: '↓',
|
||||
left: '←',
|
||||
right: '→',
|
||||
space: '␣',
|
||||
};
|
||||
|
||||
// Map of special keys to their Windows display text
|
||||
const windowsKeyMap: Record<string, string> = {
|
||||
command: 'Win',
|
||||
cmd: 'Win',
|
||||
option: 'Alt',
|
||||
alt: 'Alt',
|
||||
ctrl: 'Ctrl',
|
||||
control: 'Ctrl',
|
||||
delete: 'Del',
|
||||
escape: 'Esc',
|
||||
esc: 'Esc',
|
||||
enter: 'Enter',
|
||||
return: 'Enter',
|
||||
tab: 'Tab',
|
||||
caps: 'Caps',
|
||||
shift: 'Shift',
|
||||
space: 'Space',
|
||||
up: '↑',
|
||||
down: '↓',
|
||||
left: '←',
|
||||
right: '→',
|
||||
};
|
||||
|
||||
export interface KbdProps extends React.HTMLAttributes<HTMLElement> {
|
||||
/** The key to display (e.g., 'cmd', 'ctrl', 'a') */
|
||||
show?: string;
|
||||
/** Platform style - 'window' or 'mac' */
|
||||
type?: 'window' | 'mac';
|
||||
/** Custom content to display (overrides automatic rendering) */
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const KbdComponent: React.FC<KbdProps> = ({
|
||||
show: keyProp,
|
||||
type = 'window',
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
// Get the display text based on the key and type
|
||||
const getKeyDisplay = (): React.ReactNode => {
|
||||
if (!keyProp || typeof keyProp !== 'string') return null;
|
||||
|
||||
const lowerKey = keyProp.toLowerCase();
|
||||
|
||||
// For Mac type, return the symbol if it exists
|
||||
if (type === 'mac') {
|
||||
return macKeyMap[lowerKey] || keyProp;
|
||||
}
|
||||
|
||||
// For Windows, return the formatted key if it exists, otherwise capitalize the first letter
|
||||
return windowsKeyMap[lowerKey] || (keyProp.charAt(0).toUpperCase() + keyProp.slice(1));
|
||||
};
|
||||
|
||||
// Determine what to render
|
||||
const renderContent = () => {
|
||||
// If children are provided, always use them
|
||||
if (children !== undefined && children !== null && children !== '') {
|
||||
return children;
|
||||
}
|
||||
// Otherwise use the generated display
|
||||
return getKeyDisplay() || keyProp || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<kbd
|
||||
className="inline-flex items-center justify-center px-2 py-1 mx-0.5 text-xs font-mono font-medium text-foreground bg-secondary/70 border rounded-md"
|
||||
{...props}
|
||||
>
|
||||
{renderContent()}
|
||||
</kbd>
|
||||
);
|
||||
};
|
||||
|
||||
// Export the component
|
||||
export const Kbd = KbdComponent;
|
||||
// Default export for backward compatibility
|
||||
export default KbdComponent;
|
||||
@@ -1,14 +0,0 @@
|
||||
import NextLink from "next/link";
|
||||
import { ComponentProps } from "react";
|
||||
|
||||
export default function Link({ href, ...props }: ComponentProps<"a">) {
|
||||
if (!href) return null;
|
||||
return (
|
||||
<NextLink
|
||||
href={href}
|
||||
{...props}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import {
|
||||
Info,
|
||||
AlertTriangle,
|
||||
ShieldAlert,
|
||||
CheckCircle2,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
const noteVariants = cva(
|
||||
"relative w-full rounded-lg border border-l-4 p-4 mb-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
note: "bg-muted/30 border-border border-l-primary/50 text-foreground [&>svg]:text-primary",
|
||||
danger: "border-destructive/20 border-l-destructive/60 bg-destructive/5 text-destructive [&>svg]:text-destructive dark:border-destructive/30",
|
||||
warning: "border-orange-500/20 border-l-orange-500/60 bg-orange-500/5 text-orange-600 dark:text-orange-400 [&>svg]:text-orange-600 dark:[&>svg]:text-orange-400",
|
||||
success: "border-emerald-500/20 border-l-emerald-500/60 bg-emerald-500/5 text-emerald-600 dark:text-emerald-400 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-400",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "note",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const iconMap = {
|
||||
note: Info,
|
||||
danger: ShieldAlert,
|
||||
warning: AlertTriangle,
|
||||
success: CheckCircle2,
|
||||
};
|
||||
|
||||
interface NoteProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof noteVariants> {
|
||||
title?: string;
|
||||
type?: "note" | "danger" | "warning" | "success";
|
||||
}
|
||||
|
||||
export default function Note({
|
||||
className,
|
||||
title = "Note",
|
||||
type = "note",
|
||||
children,
|
||||
...props
|
||||
}: NoteProps) {
|
||||
const Icon = iconMap[type] || Info;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(noteVariants({ variant: type }), className)}
|
||||
{...props}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<div className="pl-8">
|
||||
<h5 className="mb-1 font-medium leading-none tracking-tight">
|
||||
{title}
|
||||
</h5>
|
||||
<div className="text-sm [&_p]:leading-relaxed opacity-90">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { BaseMdxFrontmatter, getAllChilds } from "@/lib/markdown";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Outlet({ path }: { path: string }) {
|
||||
if (!path) throw new Error("path not provided");
|
||||
const output = await getAllChilds(path);
|
||||
|
||||
return (
|
||||
<div className="grid md:grid-cols-2 gap-5">
|
||||
{output.map((child) => (
|
||||
<ChildCard {...child} key={child.title} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ChildCardProps = BaseMdxFrontmatter & { href: string };
|
||||
|
||||
function ChildCard({ description, href, title }: ChildCardProps) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="border rounded-md p-4 no-underline flex flex-col gap-0.5"
|
||||
>
|
||||
<h4 className="!my-0">{title}</h4>
|
||||
<p className="text-sm text-muted-foreground !my-0">{description}</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { type ComponentProps, type JSX } from "react";
|
||||
import Copy from "./CopyMdx";
|
||||
import {
|
||||
SiJavascript,
|
||||
SiTypescript,
|
||||
SiReact,
|
||||
SiPython,
|
||||
SiGo,
|
||||
SiPhp,
|
||||
SiRuby,
|
||||
SiSwift,
|
||||
SiKotlin,
|
||||
SiHtml5,
|
||||
SiCss,
|
||||
SiSass,
|
||||
SiPostgresql,
|
||||
SiGraphql,
|
||||
SiYaml,
|
||||
SiToml,
|
||||
SiDocker,
|
||||
SiNginx,
|
||||
SiGit,
|
||||
SiGnubash,
|
||||
SiMarkdown,
|
||||
} from "react-icons/si";
|
||||
import { FaJava, FaCode } from "react-icons/fa";
|
||||
import { TbJson } from "react-icons/tb";
|
||||
|
||||
type PreProps = ComponentProps<"pre"> & {
|
||||
raw?: string;
|
||||
"data-title"?: string;
|
||||
};
|
||||
|
||||
// Component to display an icon based on the programming language
|
||||
const LanguageIcon = ({ lang }: { lang: string }) => {
|
||||
const iconProps = { className: "w-4 h-4" };
|
||||
const languageToIconMap: Record<string, JSX.Element> = {
|
||||
gitignore: <SiGit {...iconProps} />,
|
||||
docker: <SiDocker {...iconProps} />,
|
||||
dockerfile: <SiDocker {...iconProps} />,
|
||||
nginx: <SiNginx {...iconProps} />,
|
||||
sql: <SiPostgresql {...iconProps} />,
|
||||
graphql: <SiGraphql {...iconProps} />,
|
||||
yaml: <SiYaml {...iconProps} />,
|
||||
yml: <SiYaml {...iconProps} />,
|
||||
toml: <SiToml {...iconProps} />,
|
||||
json: <TbJson {...iconProps} />,
|
||||
md: <SiMarkdown {...iconProps} />,
|
||||
markdown: <SiMarkdown {...iconProps} />,
|
||||
bash: <SiGnubash {...iconProps} />,
|
||||
sh: <SiGnubash {...iconProps} />,
|
||||
shell: <SiGnubash {...iconProps} />,
|
||||
swift: <SiSwift {...iconProps} />,
|
||||
kotlin: <SiKotlin {...iconProps} />,
|
||||
kt: <SiKotlin {...iconProps} />,
|
||||
kts: <SiKotlin {...iconProps} />,
|
||||
rb: <SiRuby {...iconProps} />,
|
||||
ruby: <SiRuby {...iconProps} />,
|
||||
php: <SiPhp {...iconProps} />,
|
||||
go: <SiGo {...iconProps} />,
|
||||
py: <SiPython {...iconProps} />,
|
||||
python: <SiPython {...iconProps} />,
|
||||
java: <FaJava {...iconProps} />,
|
||||
tsx: <SiReact {...iconProps} />,
|
||||
typescript: <SiTypescript {...iconProps} />,
|
||||
ts: <SiTypescript {...iconProps} />,
|
||||
jsx: <SiReact {...iconProps} />,
|
||||
js: <SiJavascript {...iconProps} />,
|
||||
javascript: <SiJavascript {...iconProps} />,
|
||||
html: <SiHtml5 {...iconProps} />,
|
||||
css: <SiCss {...iconProps} />,
|
||||
scss: <SiSass {...iconProps} />,
|
||||
sass: <SiSass {...iconProps} />,
|
||||
};
|
||||
return languageToIconMap[lang] || <FaCode {...iconProps} />;
|
||||
};
|
||||
|
||||
// Function to extract the language from className
|
||||
function getLanguage(className: string = ""): string {
|
||||
const match = className.match(/language-(\w+)/);
|
||||
return match ? match[1] : "default";
|
||||
}
|
||||
|
||||
export default function Pre({ children, raw, ...rest }: PreProps) {
|
||||
const { "data-title": title, className, ...restProps } = rest;
|
||||
const language = getLanguage(className);
|
||||
const hasTitle = !!title;
|
||||
|
||||
return (
|
||||
<div className="code-block-container">
|
||||
<div className="code-block-actions">
|
||||
{raw && <Copy content={raw} />}
|
||||
</div>
|
||||
{hasTitle && (
|
||||
<div className="code-block-header">
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageIcon lang={language} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="code-block-body">
|
||||
<pre className={className} {...restProps}>
|
||||
{children}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PlusCircle, Wrench, Zap, AlertTriangle, XCircle } from 'lucide-react';
|
||||
|
||||
interface ReleaseProps extends PropsWithChildren {
|
||||
version: string;
|
||||
title: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
function Release({ version, title, date, children }: ReleaseProps) {
|
||||
|
||||
return (
|
||||
<div className="mb-16 group">
|
||||
<div className="flex items-center gap-3 mt-6 mb-2">
|
||||
<div
|
||||
id={version}
|
||||
className="inline-flex items-center rounded-full border border-primary/20 bg-primary/10 px-3 py-1 text-sm font-semibold text-primary transition-colors hover:bg-primary/15 scroll-m-20 backdrop-blur-sm"
|
||||
>
|
||||
v{version}
|
||||
</div>
|
||||
{date && (
|
||||
<div className="flex items-center gap-3 text-sm font-medium text-muted-foreground">
|
||||
<span className="h-1 w-1 rounded-full bg-muted-foreground/30"></span>
|
||||
<time dateTime={date}>
|
||||
{new Date(date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-foreground/90 mb-6 mt-0!">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="space-y-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChangesProps extends PropsWithChildren {
|
||||
type: 'added' | 'fixed' | 'improved' | 'deprecated' | 'removed';
|
||||
}
|
||||
|
||||
const typeConfig = {
|
||||
added: {
|
||||
label: 'Added',
|
||||
className: 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300',
|
||||
icon: PlusCircle,
|
||||
},
|
||||
fixed: {
|
||||
label: 'Fixed',
|
||||
className: 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300',
|
||||
icon: Wrench,
|
||||
},
|
||||
improved: {
|
||||
label: 'Improved',
|
||||
className: 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300',
|
||||
icon: Zap,
|
||||
},
|
||||
deprecated: {
|
||||
label: 'Deprecated',
|
||||
className: 'bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300',
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
removed: {
|
||||
label: 'Removed',
|
||||
className: 'bg-pink-100 dark:bg-pink-900/50 text-pink-700 dark:text-pink-300',
|
||||
icon: XCircle,
|
||||
},
|
||||
} as const;
|
||||
|
||||
function Changes({ type, children }: ChangesProps) {
|
||||
const config = typeConfig[type] || typeConfig.added;
|
||||
|
||||
return (
|
||||
<div className="space-y-3 mb-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn("px-3 py-1 rounded-full text-sm font-medium flex items-center gap-1.5", config.className)}>
|
||||
<config.icon className="h-3.5 w-3.5" />
|
||||
<span>{config.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="list-none pl-0 space-y-2 text-foreground/80">
|
||||
{React.Children.map(children, (child, index) => {
|
||||
// Jika teks dimulai dengan - atau *, hapus karakter tersebut
|
||||
const processedChild = typeof child === 'string'
|
||||
? child.trim().replace(/^[-*]\s+/, '')
|
||||
: child;
|
||||
|
||||
return (
|
||||
<li key={index} className="leading-relaxed">
|
||||
{processedChild}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Release, Changes };
|
||||
|
||||
const ReleaseMdx = {
|
||||
Release,
|
||||
Changes
|
||||
};
|
||||
|
||||
export default ReleaseMdx;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { Children, PropsWithChildren } from "react";
|
||||
|
||||
export function Stepper({ children }: PropsWithChildren) {
|
||||
const length = Children.count(children);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{Children.map(children, (child, index) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-l border-muted pl-9 ml-3 relative",
|
||||
clsx({
|
||||
"pb-5 ": index < length - 1,
|
||||
})
|
||||
)}
|
||||
>
|
||||
<div className="bg-muted text-muted-foreground w-8 h-8 text-xs font-medium rounded-md border border-border/50 flex items-center justify-center absolute -left-4 font-code">
|
||||
{index + 1}
|
||||
</div>
|
||||
{child}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StepperItem({
|
||||
children,
|
||||
title,
|
||||
}: PropsWithChildren & { title?: string }) {
|
||||
return (
|
||||
<div className="pt-0.5">
|
||||
<h4 className="mt-0">{title}</h4>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface TooltipProps {
|
||||
text: string;
|
||||
tip: string;
|
||||
}
|
||||
|
||||
const Tooltip: React.FC<TooltipProps> = ({ text, tip }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="relative inline-flex items-center cursor-help text-primary hover:text-primary/80 transition-colors"
|
||||
onMouseEnter={() => setVisible(true)}
|
||||
onMouseLeave={() => setVisible(false)}
|
||||
>
|
||||
<span className="border-b border-dashed border-primary/60 pb-0.5">
|
||||
{text}
|
||||
</span>
|
||||
{visible && (
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-64 bg-popover text-popover-foreground text-sm p-3 rounded-md shadow-lg border border-border/50 wrap-break-word text-left z-50">
|
||||
{tip}
|
||||
<span className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-popover rotate-45 border-b border-r border-border/50 -z-10" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface YoutubeProps {
|
||||
videoId: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Youtube: React.FC<YoutubeProps> = ({ videoId, className }) => {
|
||||
return (
|
||||
<div className={`youtube ${className || ""}`}>
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${videoId}?rel=0&modestbranding=1&showinfo=0&autohide=1&controls=1`}
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Youtube;
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||
import { Kbd } from './KeyboardMdx';
|
||||
|
||||
// Define components mapping
|
||||
const components = {
|
||||
// Keyboard components
|
||||
Kbd: Kbd as React.ComponentType<React.HTMLAttributes<HTMLElement> & { type?: 'window' | 'mac' }>,
|
||||
kbd: Kbd as React.ComponentType<React.HTMLAttributes<HTMLElement> & { type?: 'window' | 'mac' }>,
|
||||
};
|
||||
|
||||
interface MDXProviderWrapperProps {
|
||||
source: string;
|
||||
}
|
||||
|
||||
export function MDXProviderWrapper({ source }: MDXProviderWrapperProps) {
|
||||
return (
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<MDXRemote
|
||||
source={source}
|
||||
components={components}
|
||||
options={{
|
||||
parseFrontmatter: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,82 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRight, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import Search from "@/components/SearchBox"
|
||||
import Anchor from "@/components/anchor"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import docuConfig from "@/docu.json"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useState, useCallback } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { ModeToggle } from "@/components/ThemeToggle"
|
||||
import { ArrowUpRight, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import Search from "@/components/SearchBox";
|
||||
import Anchor from "@/components/anchor";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import docuConfig from "@/docu.json";
|
||||
import GitHubButton from "@/components/Github";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { ModeToggle } from "@/components/ThemeToggle";
|
||||
|
||||
interface NavbarProps {
|
||||
id?: string
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function Navbar({ id }: NavbarProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const navRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setIsMenuOpen((prev) => !prev)
|
||||
}, [])
|
||||
setIsMenuOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// Close menu when the user clicks/taps anywhere outside the navbar, or presses Escape
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) return;
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (navRef.current && !navRef.current.contains(event.target as Node)) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") setIsMenuOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
// Focus trap: keep Tab within the mobile menu when open
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen || !menuRef.current) return;
|
||||
const menu = menuRef.current;
|
||||
const focusableSelector =
|
||||
'a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
const handleTrap = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Tab") return;
|
||||
const focusables = menu.querySelectorAll<HTMLElement>(focusableSelector);
|
||||
if (focusables.length === 0) return;
|
||||
const first = focusables[0];
|
||||
const last = focusables[focusables.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Move focus into the menu
|
||||
const focusables = menu.querySelectorAll<HTMLElement>(focusableSelector);
|
||||
if (focusables.length > 0) focusables[0].focus();
|
||||
|
||||
menu.addEventListener("keydown", handleTrap);
|
||||
return () => menu.removeEventListener("keydown", handleTrap);
|
||||
}, [isMenuOpen]);
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-50 w-full">
|
||||
<div ref={navRef} className="sticky top-0 z-50 w-full">
|
||||
<nav id={id} className="bg-background h-16 w-full border-b">
|
||||
<div className="mx-auto flex h-full w-[95vw] items-center justify-between sm:container md:gap-2">
|
||||
<div className="flex items-center gap-6">
|
||||
@@ -42,6 +94,7 @@ export function Navbar({ id }: NavbarProps) {
|
||||
onClick={toggleMenu}
|
||||
aria-label={isMenuOpen ? "Close navigation menu" : "Open navigation menu"}
|
||||
aria-expanded={isMenuOpen}
|
||||
aria-controls="mobile-nav-menu"
|
||||
className="flex items-center gap-1 px-2 text-sm font-medium md:hidden"
|
||||
>
|
||||
{isMenuOpen ? (
|
||||
@@ -50,38 +103,47 @@ export function Navbar({ id }: NavbarProps) {
|
||||
<ChevronDown className="text-muted-foreground h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Separator className="my-4 hidden h-9 md:flex" orientation="vertical" />
|
||||
<Search />
|
||||
<div className="hidden md:flex">
|
||||
<GitHubButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<AnimatePresence>
|
||||
{isMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
className="bg-background/95 w-full border-b shadow-sm backdrop-blur-sm md:hidden"
|
||||
>
|
||||
<div className="mx-auto w-[95vw] sm:container">
|
||||
<ul className="flex flex-col py-2">
|
||||
<NavMenuCollapsible onItemClick={() => setIsMenuOpen(false)} />
|
||||
</ul>
|
||||
<div className="flex items-center justify-between border-t px-1 py-3">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
<div
|
||||
id="mobile-nav-menu"
|
||||
ref={menuRef}
|
||||
role="dialog"
|
||||
aria-modal={isMenuOpen ? true : undefined}
|
||||
aria-label="Navigation menu"
|
||||
className="bg-background/95 grid w-full border-b shadow-sm backdrop-blur-sm transition-[grid-template-rows,opacity] duration-200 ease-in-out md:hidden"
|
||||
style={{
|
||||
gridTemplateRows: isMenuOpen ? "1fr" : "0fr",
|
||||
opacity: isMenuOpen ? 1 : 0,
|
||||
borderBottomWidth: isMenuOpen ? undefined : 0,
|
||||
}}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="mx-auto w-[95vw] sm:container">
|
||||
<ul className="flex flex-col py-2">
|
||||
<NavMenuCollapsible onItemClick={() => setIsMenuOpen(false)} />
|
||||
</ul>
|
||||
<div className="flex items-center justify-between border-t px-1 py-3">
|
||||
<GitHubButton />
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function Logo() {
|
||||
const { navbar } = docuConfig
|
||||
const { navbar } = docuConfig;
|
||||
|
||||
return (
|
||||
<Link href="/" className="flex items-center gap-1.5">
|
||||
@@ -98,17 +160,17 @@ export function Logo() {
|
||||
{navbar.logoText}
|
||||
</h2>
|
||||
</Link>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop NavMenu — horizontal list
|
||||
export function NavMenu() {
|
||||
const { navbar } = docuConfig
|
||||
const { navbar } = docuConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
{navbar?.menu?.map((item) => {
|
||||
const isExternal = item.href.startsWith("http")
|
||||
const isExternal = item.href.startsWith("http");
|
||||
return (
|
||||
<Anchor
|
||||
key={`${item.title}-${item.href}`}
|
||||
@@ -122,20 +184,20 @@ export function NavMenu() {
|
||||
{item.title}
|
||||
{isExternal && <ArrowUpRight className="text-foreground/80 h-4 w-4" />}
|
||||
</Anchor>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile Collapsible NavMenu — vertical list items
|
||||
function NavMenuCollapsible({ onItemClick }: { onItemClick: () => void }) {
|
||||
const { navbar } = docuConfig
|
||||
const { navbar } = docuConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
{navbar?.menu?.map((item) => {
|
||||
const isExternal = item.href.startsWith("http")
|
||||
const isExternal = item.href.startsWith("http");
|
||||
return (
|
||||
<li key={item.title + item.href}>
|
||||
<Anchor
|
||||
@@ -151,8 +213,8 @@ function NavMenuCollapsible({ onItemClick }: { onItemClick: () => void }) {
|
||||
{isExternal && <ArrowUpRight className="text-foreground/60 h-4 w-4 shrink-0" />}
|
||||
</Anchor>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { EachRoute } from "@/lib/routes";
|
||||
import Anchor from "./anchor";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SheetClose } from "@/components/ui/sheet";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface SubLinkProps extends EachRoute {
|
||||
@@ -27,42 +23,44 @@ export default function SubLink({
|
||||
parentHref = "",
|
||||
}: SubLinkProps) {
|
||||
const path = usePathname();
|
||||
const [isOpen, setIsOpen] = useState(level === 0);
|
||||
|
||||
// Full path including parent's href
|
||||
const fullHref = `${parentHref}${href}`;
|
||||
const fullHref = parentHref ? `${parentHref}${href}` : `/docs${href}`;
|
||||
|
||||
const shouldBeOpen = level === 0 || (!!items && path.startsWith(fullHref) && path !== fullHref);
|
||||
const [isOpen, setIsOpen] = useState(shouldBeOpen);
|
||||
|
||||
// Check if any child is active (for parent items)
|
||||
const hasActiveChild = useMemo(() => {
|
||||
if (!items) return false;
|
||||
return items.some(item => {
|
||||
return items.some((item) => {
|
||||
const childHref = `${fullHref}${item.href}`;
|
||||
return path.startsWith(childHref) && path !== fullHref;
|
||||
});
|
||||
}, [items, path, fullHref]);
|
||||
|
||||
// Auto-expand if current path is a child of this item
|
||||
useEffect(() => {
|
||||
if (items && (path.startsWith(fullHref) && path !== fullHref)) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsOpen(true);
|
||||
}
|
||||
}, [path, fullHref, items]);
|
||||
// Sync open state when path changes (expand if child becomes active)
|
||||
if (shouldBeOpen && !isOpen) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
// Only apply active styles if it's an exact match and not a parent with active children
|
||||
const Comp = useMemo(() => (
|
||||
<Anchor
|
||||
activeClassName={!hasActiveChild ? "dark:text-accent text-primary font-medium" : ""}
|
||||
href={fullHref}
|
||||
data-search-lvl0={level === 0 && hasActiveChild ? "true" : undefined}
|
||||
className={cn(
|
||||
"text-foreground/80 hover:text-foreground transition-colors",
|
||||
hasActiveChild && "font-medium text-foreground"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</Anchor>
|
||||
), [title, fullHref, hasActiveChild, level]);
|
||||
const Comp = useMemo(
|
||||
() => (
|
||||
<Anchor
|
||||
activeClassName={!hasActiveChild ? "dark:text-accent text-primary font-medium" : ""}
|
||||
href={fullHref}
|
||||
data-search-lvl0={level === 0 && hasActiveChild ? "true" : undefined}
|
||||
className={cn(
|
||||
"text-foreground/80 hover:text-foreground transition-colors",
|
||||
hasActiveChild && "text-foreground font-medium"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</Anchor>
|
||||
),
|
||||
[title, fullHref, hasActiveChild, level]
|
||||
);
|
||||
|
||||
const titleOrLink = !noLink ? (
|
||||
isSheet ? (
|
||||
@@ -74,7 +72,7 @@ export default function SubLink({
|
||||
<h4
|
||||
data-search-lvl0={level === 0 && hasActiveChild ? "true" : undefined}
|
||||
className={cn(
|
||||
"font-medium sm:text-sm text-foreground/90 hover:text-foreground transition-colors",
|
||||
"text-foreground/90 hover:text-foreground font-medium transition-colors sm:text-sm",
|
||||
hasActiveChild ? "text-foreground" : "text-foreground/80"
|
||||
)}
|
||||
>
|
||||
@@ -87,26 +85,25 @@ export default function SubLink({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-1 w-full")}>
|
||||
<div className={cn("flex w-full flex-col gap-1")}>
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger
|
||||
className="w-full pr-5 text-left cursor-pointer"
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
{titleOrLink}
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
{!isOpen ? (
|
||||
<ChevronRight className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronDown className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{titleOrLink}
|
||||
<CollapsibleTrigger
|
||||
className="text-muted-foreground ml-2 cursor-pointer"
|
||||
aria-expanded={isOpen}
|
||||
aria-label={`Toggle ${title} section`}
|
||||
aria-controls={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, "-")}`}
|
||||
>
|
||||
{!isOpen ? (
|
||||
<ChevronRight className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronDown className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent
|
||||
id={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
||||
id={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, "-")}`}
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-200 ease-in-out",
|
||||
isOpen ? "animate-collapsible-down" : "animate-collapsible-up"
|
||||
@@ -114,8 +111,8 @@ export default function SubLink({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-start sm:text-sm text-foreground/80 ml-0.5 mt-2.5 gap-3 hover:[&_a]:text-foreground transition-colors",
|
||||
level > 0 && "pl-4 border-l border-border ml-1.5"
|
||||
"text-foreground/80 hover:[&_a]:text-foreground mt-2.5 ml-0.5 flex flex-col items-start gap-3 transition-colors sm:text-sm",
|
||||
level > 0 && "border-border ml-1.5 border-l pl-4"
|
||||
)}
|
||||
>
|
||||
{items?.map((innerLink) => (
|
||||
|
||||
@@ -6,23 +6,45 @@ import { ListIcon } from "lucide-react"
|
||||
import Sponsor from "./Sponsor"
|
||||
import { useActiveSection } from "@/hooks"
|
||||
import { TocItem } from "@/lib/toc"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export default function Toc({ tocs }: { tocs: TocItem[] }) {
|
||||
const { activeId, setActiveId } = useActiveSection(tocs)
|
||||
const hasTocs = Boolean(tocs?.length)
|
||||
|
||||
const wrapperClassName = cn(
|
||||
hasTocs ? "toc" : "",
|
||||
"flex-3 sticky top-4 hidden h-[calc(100vh-8rem)] min-w-[240px] self-start lg:flex lg:p-8"
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="toc flex-3 sticky top-4 hidden h-[calc(100vh-8rem)] min-w-[238px] self-start lg:flex lg:p-8">
|
||||
<div className="mb-auto flex h-full w-full flex-col gap-2 px-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<ListIcon className="h-4 w-4" />
|
||||
<h3 className="text-sm font-medium">On this page</h3>
|
||||
</div>
|
||||
<div className="max-h-[calc(70vh-2rem)] min-h-0 shrink-0">
|
||||
<ScrollArea className="h-full">
|
||||
<TocObserver data={tocs} activeId={activeId} onActiveIdChange={setActiveId} />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<Sponsor />
|
||||
<div className={wrapperClassName}>
|
||||
<div className="flex h-full w-full flex-col gap-2 px-2">
|
||||
{hasTocs && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<ListIcon className="h-4 w-4" />
|
||||
<h3 className="text-sm font-medium">On this page</h3>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-full">
|
||||
<TocObserver data={tocs} activeId={activeId} onActiveIdChange={setActiveId} />
|
||||
<div className="mt-4">
|
||||
<Sponsor />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!hasTocs && (
|
||||
<div className="flex-1 min-h-0">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="mt-4">
|
||||
<Sponsor />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -24,12 +24,12 @@ const AnimatedShinyText: FC<AnimatedShinyTextProps> = ({
|
||||
"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70",
|
||||
|
||||
// Shine effect
|
||||
"animate-shiny-text bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shiny-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
|
||||
"animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat",
|
||||
|
||||
// Shine gradient
|
||||
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
|
||||
// Shine gradient (aurora colors)
|
||||
"bg-gradient-to-r from-[#FF0080] via-[#0070F3] via-[#7928CA] to-[#38bdf8] dark:from-[#FF0080] dark:via-[#0070F3] dark:via-[#7928CA] dark:to-[#38bdf8]",
|
||||
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { memo } from "react";
|
||||
|
||||
interface AuroraTextProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
colors?: string[];
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export const AuroraText = memo(
|
||||
({
|
||||
children,
|
||||
className = "",
|
||||
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
||||
speed = 1,
|
||||
}: AuroraTextProps) => {
|
||||
const gradientStyle = {
|
||||
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
||||
colors[0]
|
||||
})`,
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
animationDuration: `${10 / speed}s`,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`relative inline-block ${className}`}>
|
||||
<span className="sr-only">{children}</span>
|
||||
<span
|
||||
className="relative animate-aurora bg-[length:200%_auto] bg-clip-text text-transparent"
|
||||
style={gradientStyle}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AuroraText.displayName = "AuroraText";
|
||||
|
||||
export default AuroraText;
|
||||
@@ -1,50 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -1,37 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -1,82 +1,68 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav"> & {
|
||||
separator?: React.ReactNode
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
||||
Breadcrumb.displayName = "Breadcrumb"
|
||||
const Breadcrumb = React.forwardRef<HTMLElement, React.ComponentPropsWithoutRef<"nav">>(
|
||||
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
|
||||
);
|
||||
Breadcrumb.displayName = "Breadcrumb";
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbList.displayName = "BreadcrumbList"
|
||||
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
BreadcrumbList.displayName = "BreadcrumbList";
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem"
|
||||
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
|
||||
)
|
||||
);
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink"
|
||||
);
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
@@ -85,13 +71,10 @@ const BreadcrumbSeparator = ({
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
@@ -101,8 +84,8 @@ const BreadcrumbEllipsis = ({
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
@@ -112,4 +95,4 @@ export {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -38,46 +38,29 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-3 top-3.5 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<div className="hidden md:flex rounded-sm text-xs border py-1 px-2 hover:bg-muted">
|
||||
Esc
|
||||
</div>
|
||||
<X className="h-5 w-5 hidden max-md:flex" />
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-3.5 right-3 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
|
||||
<div className="hover:bg-muted hidden rounded-sm border px-2 py-1 text-xs md:flex">Esc</div>
|
||||
<X className="hidden h-5 w-5 max-md:flex" />
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -89,10 +72,7 @@ const DialogTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
className={cn("text-lg leading-none font-semibold tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -104,7 +84,7 @@ const DialogDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
|
||||
interface Icon {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
scale: number;
|
||||
opacity: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface IconCloudProps {
|
||||
icons?: React.ReactNode[];
|
||||
images?: string[];
|
||||
}
|
||||
|
||||
function easeOutCubic(t: number): number {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
export function IconCloud({ icons, images }: IconCloudProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
// const [iconPositions, setIconPositions] = useState<Icon[]>([]);
|
||||
const iconPositions = useMemo<Icon[]>(() => {
|
||||
const items = icons || images || [];
|
||||
const newIcons: Icon[] = [];
|
||||
const numIcons = items.length || 20;
|
||||
|
||||
// Fibonacci sphere parameters
|
||||
const offset = 2 / numIcons;
|
||||
const increment = Math.PI * (3 - Math.sqrt(5));
|
||||
|
||||
for (let i = 0; i < numIcons; i++) {
|
||||
const y = i * offset - 1 + offset / 2;
|
||||
const r = Math.sqrt(1 - y * y);
|
||||
const phi = i * increment;
|
||||
|
||||
const x = Math.cos(phi) * r;
|
||||
const z = Math.sin(phi) * r;
|
||||
|
||||
newIcons.push({
|
||||
x: x * 100,
|
||||
y: y * 100,
|
||||
z: z * 100,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
id: i,
|
||||
});
|
||||
}
|
||||
return newIcons;
|
||||
}, [icons, images]);
|
||||
const [rotation] = useState({ x: 0, y: 0 });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 });
|
||||
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
||||
const [targetRotation, setTargetRotation] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
distance: number;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
} | null>(null);
|
||||
const animationFrameRef = useRef<number>(undefined);
|
||||
const rotationRef = useRef(rotation);
|
||||
const iconCanvasesRef = useRef<HTMLCanvasElement[]>([]);
|
||||
const imagesLoadedRef = useRef<boolean[]>([]);
|
||||
|
||||
// Create icon canvases once when icons/images change
|
||||
useEffect(() => {
|
||||
if (!icons && !images) return;
|
||||
|
||||
const items = icons || images || [];
|
||||
imagesLoadedRef.current = new Array(items.length).fill(false);
|
||||
|
||||
const newIconCanvases = items.map((item, index) => {
|
||||
const offscreen = document.createElement("canvas");
|
||||
offscreen.width = 40;
|
||||
offscreen.height = 40;
|
||||
const offCtx = offscreen.getContext("2d");
|
||||
|
||||
if (offCtx) {
|
||||
if (images) {
|
||||
// Handle image URLs directly
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.src = items[index] as string;
|
||||
img.onload = () => {
|
||||
offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
|
||||
|
||||
// Create circular clipping path
|
||||
offCtx.beginPath();
|
||||
offCtx.arc(20, 20, 20, 0, Math.PI * 2);
|
||||
offCtx.closePath();
|
||||
offCtx.clip();
|
||||
|
||||
// Draw the image
|
||||
offCtx.drawImage(img, 0, 0, 40, 40);
|
||||
|
||||
imagesLoadedRef.current[index] = true;
|
||||
};
|
||||
} else {
|
||||
// Handle SVG icons
|
||||
offCtx.scale(0.4, 0.4);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const svgString = renderToString(item as React.ReactElement<any>);
|
||||
const img = new Image();
|
||||
img.src = "data:image/svg+xml;base64," + btoa(svgString);
|
||||
img.onload = () => {
|
||||
offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
|
||||
offCtx.drawImage(img, 0, 0);
|
||||
imagesLoadedRef.current[index] = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
return offscreen;
|
||||
});
|
||||
|
||||
iconCanvasesRef.current = newIconCanvases;
|
||||
}, [icons, images]);
|
||||
|
||||
// Generate initial icon positions on a sphere
|
||||
// useEffect(() => {
|
||||
// const items = icons || images || [];
|
||||
// const newIcons: Icon[] = [];
|
||||
// const numIcons = items.length || 20;
|
||||
|
||||
// // Fibonacci sphere parameters
|
||||
// const offset = 2 / numIcons;
|
||||
// const increment = Math.PI * (3 - Math.sqrt(5));
|
||||
|
||||
// for (let i = 0; i < numIcons; i++) {
|
||||
// const y = i * offset - 1 + offset / 2;
|
||||
// const r = Math.sqrt(1 - y * y);
|
||||
// const phi = i * increment;
|
||||
|
||||
// const x = Math.cos(phi) * r;
|
||||
// const z = Math.sin(phi) * r;
|
||||
|
||||
// newIcons.push({
|
||||
// x: x * 100,
|
||||
// y: y * 100,
|
||||
// z: z * 100,
|
||||
// scale: 1,
|
||||
// opacity: 1,
|
||||
// id: i,
|
||||
// });
|
||||
// }
|
||||
// setIconPositions(newIcons);
|
||||
// }, [icons, images]);
|
||||
|
||||
// Handle mouse events
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect || !canvasRef.current) return;
|
||||
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const ctx = canvasRef.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
iconPositions.forEach((icon) => {
|
||||
const cosX = Math.cos(rotationRef.current.x);
|
||||
const sinX = Math.sin(rotationRef.current.x);
|
||||
const cosY = Math.cos(rotationRef.current.y);
|
||||
const sinY = Math.sin(rotationRef.current.y);
|
||||
|
||||
const rotatedX = icon.x * cosY - icon.z * sinY;
|
||||
const rotatedZ = icon.x * sinY + icon.z * cosY;
|
||||
const rotatedY = icon.y * cosX + rotatedZ * sinX;
|
||||
|
||||
const screenX = canvasRef.current!.width / 2 + rotatedX;
|
||||
const screenY = canvasRef.current!.height / 2 + rotatedY;
|
||||
|
||||
const scale = (rotatedZ + 200) / 300;
|
||||
const radius = 20 * scale;
|
||||
const dx = x - screenX;
|
||||
const dy = y - screenY;
|
||||
|
||||
if (dx * dx + dy * dy < radius * radius) {
|
||||
const targetX = -Math.atan2(
|
||||
icon.y,
|
||||
Math.sqrt(icon.x * icon.x + icon.z * icon.z),
|
||||
);
|
||||
const targetY = Math.atan2(icon.x, icon.z);
|
||||
|
||||
const currentX = rotationRef.current.x;
|
||||
const currentY = rotationRef.current.y;
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2),
|
||||
);
|
||||
|
||||
const duration = Math.min(2000, Math.max(800, distance * 1000));
|
||||
|
||||
setTargetRotation({
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
startX: currentX,
|
||||
startY: currentY,
|
||||
distance,
|
||||
startTime: performance.now(),
|
||||
duration,
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
setIsDragging(true);
|
||||
setLastMousePos({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
setMousePos({ x, y });
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
const deltaX = e.clientX - lastMousePos.x;
|
||||
const deltaY = e.clientY - lastMousePos.y;
|
||||
|
||||
rotationRef.current = {
|
||||
x: rotationRef.current.x + deltaY * 0.002,
|
||||
y: rotationRef.current.y + deltaX * 0.002,
|
||||
};
|
||||
|
||||
setLastMousePos({ x: e.clientX, y: e.clientY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
// Animation and rendering
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const centerX = canvas.width / 2;
|
||||
const centerY = canvas.height / 2;
|
||||
const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
|
||||
const dx = mousePos.x - centerX;
|
||||
const dy = mousePos.y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const speed = 0.003 + (distance / maxDistance) * 0.01;
|
||||
|
||||
if (targetRotation) {
|
||||
const elapsed = performance.now() - targetRotation.startTime;
|
||||
const progress = Math.min(1, elapsed / targetRotation.duration);
|
||||
const easedProgress = easeOutCubic(progress);
|
||||
|
||||
rotationRef.current = {
|
||||
x:
|
||||
targetRotation.startX +
|
||||
(targetRotation.x - targetRotation.startX) * easedProgress,
|
||||
y:
|
||||
targetRotation.startY +
|
||||
(targetRotation.y - targetRotation.startY) * easedProgress,
|
||||
};
|
||||
|
||||
if (progress >= 1) {
|
||||
setTargetRotation(null);
|
||||
}
|
||||
} else if (!isDragging) {
|
||||
rotationRef.current = {
|
||||
x: rotationRef.current.x + (dy / canvas.height) * speed,
|
||||
y: rotationRef.current.y + (dx / canvas.width) * speed,
|
||||
};
|
||||
}
|
||||
|
||||
iconPositions.forEach((icon, index) => {
|
||||
const cosX = Math.cos(rotationRef.current.x);
|
||||
const sinX = Math.sin(rotationRef.current.x);
|
||||
const cosY = Math.cos(rotationRef.current.y);
|
||||
const sinY = Math.sin(rotationRef.current.y);
|
||||
|
||||
const rotatedX = icon.x * cosY - icon.z * sinY;
|
||||
const rotatedZ = icon.x * sinY + icon.z * cosY;
|
||||
const rotatedY = icon.y * cosX + rotatedZ * sinX;
|
||||
|
||||
const scale = (rotatedZ + 200) / 300;
|
||||
const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200));
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(
|
||||
canvas.width / 2 + rotatedX,
|
||||
canvas.height / 2 + rotatedY,
|
||||
);
|
||||
ctx.scale(scale, scale);
|
||||
ctx.globalAlpha = opacity;
|
||||
|
||||
if (icons || images) {
|
||||
// Only try to render icons/images if they exist
|
||||
if (
|
||||
iconCanvasesRef.current[index] &&
|
||||
imagesLoadedRef.current[index]
|
||||
) {
|
||||
ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40);
|
||||
}
|
||||
} else {
|
||||
// Show numbered circles if no icons/images are provided
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 20, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#4444ff";
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "white";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = "16px Arial";
|
||||
ctx.fillText(`${icon.id + 1}`, 0, 0);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [icons, images, iconPositions, isDragging, mousePos, targetRotation]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={400}
|
||||
height={400}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
className="rounded-full"
|
||||
aria-label="Interactive 3D Icon Cloud"
|
||||
role="img"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type InteractiveHoverButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export const InteractiveHoverButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
InteractiveHoverButtonProps
|
||||
>(({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group relative w-auto cursor-pointer overflow-hidden rounded-full border bg-background p-2 px-6 text-center font-semibold",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-primary transition-all duration-300 group-hover:scale-[100.8]"></div>
|
||||
<span className="inline-block transition-all duration-300 group-hover:translate-x-12 group-hover:opacity-0">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute top-0 z-10 flex h-full w-full translate-x-12 items-center justify-center gap-2 text-primary-foreground opacity-0 transition-all duration-300 group-hover:-translate-x-5 group-hover:opacity-100">
|
||||
<span>{children}</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
InteractiveHoverButton.displayName = "InteractiveHoverButton";
|
||||
@@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -40,7 +40,7 @@ const sheetVariants = cva(
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -50,8 +50,9 @@ const sheetVariants = cva(
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> { }
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
@@ -59,14 +60,10 @@ const SheetContent = React.forwardRef<
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute top-7 right-4 z-50 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<PanelRightClose className="w-6 h-6 shrink-0 text-muted-foreground" />
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-7 right-4 z-50 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
|
||||
<PanelRightClose className="text-muted-foreground h-6 w-6 shrink-0" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
@@ -74,29 +71,14 @@ const SheetContent = React.forwardRef<
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -108,7 +90,7 @@ const SheetTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
className={cn("text-foreground text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -120,7 +102,7 @@ const SheetDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ShineBorderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Width of the border in pixels
|
||||
* @default 1
|
||||
*/
|
||||
borderWidth?: number;
|
||||
/**
|
||||
* Duration of the animation in seconds
|
||||
* @default 14
|
||||
*/
|
||||
duration?: number;
|
||||
/**
|
||||
* Color of the border, can be a single color or an array of colors
|
||||
* @default "#000000"
|
||||
*/
|
||||
shineColor?: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shine Border
|
||||
*
|
||||
* An animated background border effect component with configurable properties.
|
||||
*/
|
||||
export function ShineBorder({
|
||||
borderWidth = 1,
|
||||
duration = 14,
|
||||
shineColor = "#000000",
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: ShineBorderProps) {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--border-width": `${borderWidth}px`,
|
||||
"--duration": `${duration}s`,
|
||||
backgroundImage: `radial-gradient(transparent,transparent, ${
|
||||
Array.isArray(shineColor) ? shineColor.join(",") : shineColor
|
||||
},transparent,transparent)`,
|
||||
backgroundSize: "300% 300%",
|
||||
mask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||
WebkitMask: `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||
WebkitMaskComposite: "xor",
|
||||
maskComposite: "exclude",
|
||||
padding: "var(--border-width)",
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position] motion-safe:animate-shine",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default ShineBorder;
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -1,117 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto border border-border rounded-lg">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm !my-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b bg-muted", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center gap-2 text-muted-foreground font-mono -mb-28 w-full border-b",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap px-1.5 py-[0.58rem] text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:border-primary border-b-2 border-transparent data-[state=active]:text-foreground font-code cursor-pointer",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
Reference in New Issue
Block a user