update versi docubook

This commit is contained in:
2025-05-04 00:06:21 +07:00
parent 2bd2c03429
commit 82c3a03d3a
34 changed files with 1038 additions and 366 deletions

View File

@@ -4,7 +4,8 @@
> **Note**: This application is a fork of [AriaDocs](https://github.com/nisabmohd/Aria-Docs), created by [Nisab Mohd](https://github.com/nisabmohd). DocuBook provides an alternative to the documentation solution found on [Mintlify](https://mintlify.com/), utilizing `.mdx` (Markdown + JSX) for content creation and management.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/mywildancloud/docubook)
[![Deploy with
Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/gitfromwildan/docubook)
## Features

View File

@@ -1,4 +1,4 @@
import { Typography } from "@/components/docs/typography";
import { Typography } from "@/components/typography";
import { buttonVariants } from "@/components/ui/button";
import { Author, getAllBlogStaticPaths, getBlogForSlug } from "@/lib/markdown";
import { ArrowLeftIcon } from "lucide-react";
@@ -6,7 +6,7 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { formatDate } from "@/lib/utils";
import { ScrollToTop } from "@/components/docs/scroll-to-top";
import { ScrollToTop } from "@/components/scroll-to-top";
type PageProps = {
params: { slug: string };

View File

@@ -16,7 +16,7 @@ export default async function BlogIndexPage() {
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
);
return (
<div className="w-full mx-auto flex flex-col gap-1 sm:min-h-[91vh] min-h-[88vh] pt-2">
<div className="w-full mx-auto flex flex-col gap-1 sm:min-h-[91vh] min-h-[88vh] py-2">
<div className="mb-7 flex flex-col gap-2">
<h1 className="text-2xl font-extrabold">
Blog Posts

View File

@@ -1,14 +1,14 @@
import { notFound } from "next/navigation";
import { getDocsForSlug, getDocsTocs } from "@/lib/markdown";
import DocsBreadcrumb from "@/components/docs/docs-breadcrumb";
import Pagination from "@/components/docs/pagination";
import Toc from "@/components/docs/toc";
import { Typography } from "@/components/docs/typography";
import EditThisPage from "@/components/docs/edit-on-github";
import DocsBreadcrumb from "@/components/docs-breadcrumb";
import Pagination from "@/components/pagination";
import Toc from "@/components/toc";
import { Typography } from "@/components/typography";
import EditThisPage from "@/components/edit-on-github";
import { formatDate2 } from "@/lib/utils";
import docuConfig from "@/docu.json";
import MobToc from "@/components/docs/mob-toc";
import { ScrollToTop } from "@/components/docs/scroll-to-top";
import MobToc from "@/components/mob-toc";
import { ScrollToTop } from "@/components/scroll-to-top";
const { meta } = docuConfig;

View File

@@ -1,4 +1,4 @@
import { Leftbar } from "@/components/docs/leftbar";
import { Leftbar } from "@/components/leftbar";
export default function DocsLayout({
children,

View File

@@ -1,9 +1,9 @@
import type { Metadata } from "next";
import { ThemeProvider } from "@/components/contexts/theme-provider";
import { Navbar } from "@/components/docs/navbar";
import { Navbar } from "@/components/navbar";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { Footer } from "@/components/docs/footer";
import { Footer } from "@/components/footer";
import docuConfig from "@/docu.json";
import { Toaster } from "@/components/ui/sonner";
import "@/styles/globals.css";

View File

@@ -29,7 +29,8 @@ import {
Laptop2,
Copy,
Download,
RotateCcw
RotateCcw,
Calendar
} from "lucide-react";
import { Button as UIButton } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -38,8 +39,23 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import {
handleParagraphClick,
handleHeading2Click,
handleHeading3Click,
handleBulletListClick,
handleNumberedListClick,
handleLinkClick,
handleImageClick,
handleBlockquoteClick,
handleCodeBlockClick,
handleTableClick,
handleNoteClick,
handleComponentClick,
handleMetadataClick,
} from "@/components/playground/MarkComponent";
import "@/styles/editor.css";
const ToolbarButton = ({ icon: Icon, label, onClick }: { icon: any, label: string, onClick?: () => void }) => (
@@ -167,9 +183,7 @@ export default function PlaygroundPage() {
</UIButton>
</div>
</div>
), {
duration: 10000,
});
), { duration: 10000 });
}
};
@@ -179,149 +193,17 @@ export default function PlaygroundPage() {
const before = markdown.substring(0, start);
const after = markdown.substring(end);
const newText = before + text + after;
// Menambahkan satu baris kosong sebelum dan sesudah komponen
const newText = `${before}${text}\n${after}`;
setMarkdown(newText);
requestAnimationFrame(() => {
textArea.focus();
const newPosition = start + text.length;
const newPosition = start + text.length + 1;
textArea.setSelectionRange(newPosition, newPosition);
});
};
const handleParagraphClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, 'this is regular text, **bold text**, *italic text*\n');
}
};
const handleHeading2Click = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '## Heading 2\n');
}
};
const handleHeading3Click = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '### Heading 3\n');
}
};
const handleBulletListClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '- List One\n- List Two\n- Other List\n');
}
};
const handleNumberedListClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '1. Number One\n2. Number Two\n3. Number Three\n');
}
};
const handleLinkClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '[Visit OpenAI](https://www.openai.com)\n');
}
};
const handleImageClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '![Alt text for the image](https://via.placeholder.com/150)\n');
}
};
const handleBlockquoteClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '> The overriding design goal for Markdown\'s formatting syntax is to make it as readable as possible.\n');
}
};
const handleCodeBlockClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, '```javascript:main.js showLineNumbers {3-4}\nfunction isRocketAboutToCrash() {\n // Check if the rocket is stable\n if (!isStable()) {\n NoCrash(); // Prevent the crash\n }\n}\n```\n');
}
};
const handleTableClick = () => {
const textArea = document.querySelector('textarea');
if (textArea) {
insertAtCursor(textArea, `| **Feature** | **Description** |
| ------------------------------- | ----------------------------------------------------- |
| MDX Support | Write interactive documentation with MDX. |
| Nested Pages | Organize content in a nested, hierarchical structure. |
| Blog Section | Include a dedicated blog section. |
| Pagination | Split content across multiple pages. |
`);
}
};
const handleNoteClick = (type: string) => {
const textArea = document.querySelector('textarea');
if (textArea) {
const noteTemplate = `<Note type="${type}" title="${type.charAt(0).toUpperCase() + type.slice(1)}">\n This is a ${type} message.\n</Note>\n`;
insertAtCursor(textArea, noteTemplate);
}
};
const handleComponentClick = (component: string) => {
const textArea = document.querySelector('textarea');
if (!textArea) return;
const templates: { [key: string]: string } = {
stepper: `<Stepper>
<StepperItem title="Step 1">
Content for step 1
</StepperItem>
<StepperItem title="Step 2">
Content for step 2
</StepperItem>
</Stepper>\n`,
card: `<Card title="Click on me" icon="Link" href="/docs/getting-started/components/button">
This is how you use a card with an icon and a link. Clicking on this card
brings you to the Card Group page.
</Card>\n`,
button: `<Button
text="Click Me"
href="#"
icon="ArrowRight"
size="md"
variation="primary"
/>\n`,
accordion: `<Accordion title="Markdown">
this is an example of plain text content from the accordion component and below is markdown ;
1. number one
2. number two
3. number three
</Accordion>\n`,
youtube: `<Youtube videoId="your-video-id" />\n`,
tooltip: `What do you know about <Tooltip text="DocuBook" tip="npx @docubook/create@latest" /> ? Create interactive nested documentations using MDX.\n`,
tabs: `<Tabs defaultValue="tab1" className="pt-5 pb-1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">
Content for tab 1
</TabsContent>
<TabsContent value="tab2">
Content for tab 2
</TabsContent>
</Tabs>\n`
};
insertAtCursor(textArea, templates[component]);
};
if (isMobile) {
return <MobileMessage />;
@@ -402,86 +284,88 @@ export default function PlaygroundPage() {
</UIButton>
</div>
<div className="flex items-center border-b p-1 bg-background">
<ToolbarButton icon={Type} label="Paragraph" onClick={handleParagraphClick} />
<ToolbarButton icon={Heading2} label="Heading 2" onClick={handleHeading2Click} />
<ToolbarButton icon={Heading3} label="Heading 3" onClick={handleHeading3Click} />
<ToolbarButton icon={List} label="Bullet List" onClick={handleBulletListClick} />
<ToolbarButton icon={ListOrdered} label="Numbered List" onClick={handleNumberedListClick} />
<ToolbarButton icon={Calendar} label="Metadata" onClick={() => handleMetadataClick(insertAtCursor)} />
<ToolbarSeparator />
<ToolbarButton icon={Code} label="Code Block" onClick={handleCodeBlockClick} />
<ToolbarButton icon={Quote} label="Blockquote" onClick={handleBlockquoteClick} />
<ToolbarButton icon={ImageIcon} label="Image" onClick={handleImageClick} />
<ToolbarButton icon={LinkIcon} label="Link" onClick={handleLinkClick} />
<ToolbarButton icon={Table} label="Table" onClick={handleTableClick} />
<ToolbarButton icon={Type} label="Paragraph" onClick={() => handleParagraphClick(insertAtCursor)} />
<ToolbarButton icon={Heading2} label="Heading 2" onClick={() => handleHeading2Click(insertAtCursor)} />
<ToolbarButton icon={Heading3} label="Heading 3" onClick={() => handleHeading3Click(insertAtCursor)} />
<ToolbarButton icon={List} label="Bullet List" onClick={() => handleBulletListClick(insertAtCursor)} />
<ToolbarButton icon={ListOrdered} label="Numbered List" onClick={() => handleNumberedListClick(insertAtCursor)} />
<ToolbarSeparator />
<ToolbarButton icon={LinkIcon} label="Link" onClick={() => handleLinkClick(insertAtCursor)} />
<ToolbarButton icon={ImageIcon} label="Image" onClick={() => handleImageClick(insertAtCursor)} />
<ToolbarButton icon={Quote} label="Blockquote" onClick={() => handleBlockquoteClick(insertAtCursor)} />
<ToolbarButton icon={Code} label="Code Block" onClick={() => handleCodeBlockClick(insertAtCursor)} />
<ToolbarButton icon={Table} label="Table" onClick={() => handleTableClick(insertAtCursor)} />
<ToolbarSeparator />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild>
<UIButton
variant="ghost"
size="sm"
className="h-8 px-2 flex items-center gap-1 font-normal"
variant="ghost"
size="sm"
className="h-8 px-2 flex items-center gap-1 font-normal"
>
<Notebook className="h-4 w-4 text-muted-foreground" />
<ChevronDown className="h-4 w-4 text-muted-foreground" />
<Notebook className="h-4 w-4" />
<ChevronDown className="h-4 w-4" />
</UIButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleNoteClick('note')}>
Note
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'note')}>
Note
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleNoteClick('danger')}>
Danger
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'danger')}>
Danger
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleNoteClick('warning')}>
Warning
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'warning')}>
Warning
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleNoteClick('success')}>
Success
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'success')}>
Success
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuContent>
</DropdownMenu>
<ToolbarSeparator />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild>
<UIButton
variant="ghost"
size="sm"
className="h-8 px-2 flex items-center gap-1 font-normal"
variant="ghost"
size="sm"
className="h-8 px-2 flex items-center gap-1 font-normal"
>
<Component className="h-4 w-4 text-muted-foreground" />
<ChevronDown className="h-4 w-4 text-muted-foreground" />
<Component className="h-4 w-4" />
<ChevronDown className="h-4 w-4" />
</UIButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleComponentClick('stepper')}>
<Rows className="h-4 w-4 mr-2" />
Stepper
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'stepper')}>
<Rows className="h-4 w-4 mr-2" />
Stepper
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('card')}>
<LayoutGrid className="h-4 w-4 mr-2" />
Card
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'card')}>
<LayoutGrid className="h-4 w-4 mr-2" />
Card
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('button')}>
<MousePointer2 className="h-4 w-4 mr-2" />
Button
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'button')}>
<MousePointer2 className="h-4 w-4 mr-2" />
Button
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('accordion')}>
<ChevronDown className="h-4 w-4 mr-2" />
Accordion
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'accordion')}>
<ChevronDown className="h-4 w-4 mr-2" />
Accordion
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('tabs')}>
<LayoutPanelTop className="h-4 w-4 mr-2" />
Tabs
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'tabs')}>
<LayoutPanelTop className="h-4 w-4 mr-2" />
Tabs
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('youtube')}>
<YoutubeIcon className="h-4 w-4 mr-2" />
Youtube
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'youtube')}>
<YoutubeIcon className="h-4 w-4 mr-2" />
Youtube
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleComponentClick('tooltip')}>
<HelpCircle className="h-4 w-4 mr-2" />
Tooltip
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'tooltip')}>
<HelpCircle className="h-4 w-4 mr-2" />
Tooltip
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
@@ -494,13 +378,13 @@ export default function PlaygroundPage() {
</div>
</div>
<textarea
ref={editorRef}
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
className="editor-textarea"
spellCheck={false}
placeholder="Start writing markdown..."
/>
ref={editorRef}
value={markdown}
onChange={(e) => setMarkdown(e.target.value)}
className="editor-textarea"
spellCheck={false}
placeholder="Type '/' for commands..."
/>
</div>
</ScrollArea>
</div>

View File

@@ -1,5 +1,5 @@
import Link from "next/link";
import { ModeToggle } from "@/components/docs/theme-toggle";
import { ModeToggle } from "@/components/theme-toggle";
import docuConfig from "@/docu.json";
import * as LucideIcons from "lucide-react"; // Import all icons

View File

@@ -1,33 +0,0 @@
"use client";
import { useState } from "react";
import { TerminalSquareIcon, ClipboardIcon, CheckIcon } from "lucide-react";
export function CopyCommand() {
const [copied, setCopied] = useState(false);
const command = "npx @docubook/create@latest";
const copyToClipboard = () => {
navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 5000);
};
return (
<div className="relative flex flex-row items-center justify-center sm:gap-2 gap-0.5 text-muted-foreground text-md mt-10 mb-12 font-code text-base font-medium group">
<TerminalSquareIcon className="w-5 h-5 mr-1 mt-0.5" />
<span className="select-all">{command}</span>
<button
onClick={copyToClipboard}
className="p-1 ml-1 transition-opacity rounded-md opacity-0 group-hover:opacity-100 hover:bg-gray-200 dark:hover:bg-gray-700"
>
{copied ? (
<CheckIcon className="w-4 h-4 text-green-500" />
) : (
<ClipboardIcon className="w-4 h-4" />
)}
</button>
</div>
);
}
export default CopyCommand;

View File

@@ -1,91 +0,0 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Play } from "lucide-react";
const scriptOutput = [
"DocuBook CLI Installer",
"✔ Enter your project directory name: docubook",
"? Choose a package manager:",
"> npm",
" pnpm",
" yarn",
" bun",
"",
":: Cloning starter from GitLab...",
"✔ Docubook project successfully created!",
"Skipping rename postcss.config.js because Bun is not installed.",
"",
"[ DocuBook Version 1.8.0 ]",
"",
"Starting the installation process...",
"Installation | ████████████████████████████████ | 100% 100/100",
"",
"Dependencies installed successfully using npm!",
"",
"┌────────────────────────────────────┐",
"│ Next Steps: │",
"│ │",
"│ 1. Navigate to project directory: │",
"│ cd docubook │",
"│ │",
"│ 2. Install dependencies: │",
"│ npm install │",
"│ │",
"│ 3. Start the development server: │",
"│ npm run dev │",
"└────────────────────────────────────┘"
];
export function RuntimeSimulator() {
const [logs, setLogs] = useState<string[]>([]);
const [running, setRunning] = useState(false);
const terminalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (running) {
setLogs([]);
scriptOutput.forEach((line, index) => {
setTimeout(() => {
setLogs((prev) => [...prev, line]);
}, index * 500);
});
}
}, [running]);
useEffect(() => {
terminalRef.current?.scrollTo({ top: terminalRef.current.scrollHeight, behavior: "smooth" });
}, [logs]);
return (
<div className="bg-gray-900 text-green-400 font-mono rounded-lg overflow-hidden w-full h-auto max-w-2xl shadow-lg">
<div className="bg-gray-800 text-gray-300 px-4 py-2 flex items-center space-x-2">
<div className="flex space-x-1">
<span className="w-3 h-3 bg-red-500 rounded-full"></span>
<span className="w-3 h-3 bg-yellow-500 rounded-full"></span>
<span className="w-3 h-3 bg-green-500 rounded-full"></span>
</div>
<span className="ml-3">docubook@localhost</span>
</div>
<div ref={terminalRef} className="p-4 md:h-[400px] h-72 overflow-y-auto text-left">
{logs.map((log, index) => (
<pre key={index} className="whitespace-pre-wrap">{log}</pre>
))}
</div>
<div className="bg-gray-800 p-2 flex items-center space-x-2">
<input
type="text"
value="npx @docubook/create@latest"
readOnly
className="bg-gray-700 text-gray-300 px-2 py-1 rounded w-full text-left"
/>
<Button onClick={() => setRunning(true)} className="bg-green-600 hover:bg-green-500 px-4 py-1">
<Play className="w-5 h-5" />
</Button>
</div>
</div>
);
}
export default RuntimeSimulator;

View File

@@ -6,10 +6,10 @@ import {
SheetTrigger,
} from "@/components/ui/sheet";
import { Logo, NavMenu } from "./navbar";
import { Button } from "../ui/button";
import { Button } from "./ui/button";
import { AlignLeftIcon } from "lucide-react";
import { FooterButtons } from "./footer";
import { DialogTitle } from "../ui/dialog";
import { DialogTitle } from "./ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import DocsMenu from "./docs-menu";
import { ModeToggle } from "./theme-toggle";

View File

@@ -1,10 +1,11 @@
import { ArrowUpRight } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import Search from "./search";
import Anchor from "./anchor";
import { SheetLeftbar } from "./leftbar";
import Search from "@/components/search";
import Anchor from "@/components/anchor";
import { SheetLeftbar } from "@/components/leftbar";
import { SheetClose } from "@/components/ui/sheet";
import { Separator } from "@/components/ui/separator";
import docuConfig from "@/docu.json"; // Import JSON
export function Navbar() {
@@ -18,13 +19,13 @@ export function Navbar() {
<div className="hidden sm:flex">
<Logo />
</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
<NavMenu />
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Separator className="hidden sm:flex my-4 h-9" orientation="vertical" />
<Search />
</div>
</div>
@@ -36,7 +37,7 @@ export function Logo() {
const { navbar } = docuConfig; // Extract navbar from JSON
return (
<Link href="/" className="flex items-center gap-2.5">
<Link href="/" className="flex items-center gap-1.5">
<Image src={navbar.logo.src} alt={navbar.logo.alt} width="24" height="24" />
<h2 className="font-bold font-code text-md">{navbar.logoText}</h2>
</Link>

View File

@@ -1,7 +1,7 @@
import { getPreviousNext } from "@/lib/markdown";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import Link from "next/link";
import { buttonVariants } from "../ui/button";
import { buttonVariants } from "./ui/button";
export default function Pagination({ pathname }: { pathname: string }) {
const res = getPreviousNext(pathname);

View File

@@ -0,0 +1,252 @@
type InsertAtCursor = (textArea: HTMLTextAreaElement, text: string) => void;
// toolbar handler
export const handleMetadataClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
const metadata = `---
title: Post Title
description: Your Post Description
date: ${new Date().toISOString().split("T")[0]}
image: example-img.png
---\n\n`;
insertAtCursor(textArea, metadata);
}
};
export const handleParagraphClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(textArea, "this is regular text, **bold text**, *italic text*\n");
}
};
export const handleHeading2Click = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(textArea, "## Heading 2\n");
}
};
export const handleHeading3Click = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(textArea, "### Heading 3\n");
}
};
export const handleBulletListClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(textArea, "- List One\n- List Two\n- Other List\n");
}
};
export const handleNumberedListClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(textArea, "1. Number One\n2. Number Two\n3. Number Three\n");
}
};
export const handleLinkClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(textArea, "[Visit OpenAI](https://www.openai.com)\n");
}
};
export const handleImageClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(textArea, "![Alt text for the image](https://via.placeholder.com/150)\n");
}
};
export const handleBlockquoteClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(textArea, "> The overriding design goal for Markdown's formatting syntax is to make it as readable as possible.\n");
}
};
export const handleCodeBlockClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(
textArea,
"```javascript:main.js showLineNumbers {3-4}\nfunction isRocketAboutToCrash() {\n // Check if the rocket is stable\n if (!isStable()) {\n NoCrash(); // Prevent the crash\n }\n}\n```\n"
);
}
};
export const handleTableClick = (insertAtCursor: InsertAtCursor) => {
const textArea = document.querySelector("textarea");
if (textArea) {
insertAtCursor(
textArea,
`| **Feature** | **Description** |
| ------------------------------- | ----------------------------------------------------- |
| MDX Support | Write interactive documentation with MDX. |
| Nested Pages | Organize content in a nested, hierarchical structure. |
| Blog Section | Include a dedicated blog section. |
| Pagination | Split content across multiple pages. |
`
);
}
};
export const handleNoteClick = (insertAtCursor: InsertAtCursor, type: string) => {
return () => {
const textArea = document.querySelector("textarea");
if (textArea) {
const noteTemplate = `<Note type="${type}" title="${type.charAt(0).toUpperCase() + type.slice(1)}">\n This is a ${type} message.\n</Note>\n`;
insertAtCursor(textArea, noteTemplate);
}
};
};
export const handleComponentClick = (insertAtCursor: InsertAtCursor, component: string) => {
return () => {
const textArea = document.querySelector("textarea");
if (!textArea) return;
const templates: { [key: string]: string } = {
stepper: `<Stepper>
<StepperItem title="Step 1">
Content for step 1
</StepperItem>
<StepperItem title="Step 2">
Content for step 2
</StepperItem>
</Stepper>\n`,
card: `<Card title="Click on me" icon="Link" href="/docs/getting-started/components/button">
This is how you use a card with an icon and a link. Clicking on this card brings you to the Card Group page.
</Card>\n`,
button: `<Button
text="Click Me"
href="#"
icon="ArrowRight"
size="md"
variation="primary"
/>\n`,
accordion: `<Accordion title="Markdown">
this is an example of plain text content from the accordion component and below is markdown ;
1. number one
2. number two
3. number three
</Accordion>\n`,
youtube: `<Youtube videoId="your-video-id" />\n`,
tooltip: `What do you know about <Tooltip text="DocuBook" tip="npx @docubook/create@latest" /> ? Create interactive nested documentations using MDX.\n`,
tabs: `<Tabs defaultValue="tab1" className="pt-5 pb-1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">
Content for tab 1
</TabsContent>
<TabsContent value="tab2">
Content for tab 2
</TabsContent>
</Tabs>\n`
};
insertAtCursor(textArea, templates[component]);
};
};
// slash command handler
export const MARK_COMPONENTS = [
{ label: "Metadata", value: `---
title: Post Title
description: Your Post Description
date: ${new Date().toISOString().split("T")[0]}
image: example-img.png
---\n\n` },
{ label: "Heading 2", value: "## Heading 2\n" },
{ label: "Heading 3", value: "### Heading 3\n" },
{ label: "Paragraph", value: "this is regular text, **bold text**, *italic text*\n" },
{ label: "Bullet List", value: "- List One\n- List Two\n- Other List\n" },
{ label: "Numbered List", value: "1. Number One\n2. Number Two\n3. Number Three\n" },
{ label: "Blockquote", value: "> The overriding design goal for Markdown's formatting syntax is to make it as readable as possible.\n" },
{ label: "Code Block", value: "```javascript:main.js showLineNumbers {3-4}\nfunction isRocketAboutToCrash() {\n // Check if the rocket is stable\n if (!isStable()) {\n NoCrash(); // Prevent the crash\n }\n}\n```\n" },
{ label: "Table", value: `| **Feature** | **Description** |
| ------------------------------- | ----------------------------------------------------- |
| MDX Support | Write interactive documentation with MDX. |
| Nested Pages | Organize content in a nested, hierarchical structure. |
| Blog Section | Include a dedicated blog section. |
| Pagination | Split content across multiple pages. |
` },
{ label: "Link", value: "[Visit OpenAI](https://www.openai.com)\n" },
{ label: "Image", value: "![Alt text for the image](https://via.placeholder.com/150)\n" },
// ⭐ Komponen Interaktif DocuBook ⭐
{ label: "Stepper", value: `<Stepper>
<StepperItem title="Step 1">
Content for step 1
</StepperItem>
<StepperItem title="Step 2">
Content for step 2
</StepperItem>
</Stepper>\n` },
{ label: "Button", value: `<Button
text="Click Me"
href="#"
icon="ArrowRight"
size="md"
variation="primary"
/>\n` },
{ label: "Accordion", value: `<Accordion title="Markdown">
This is an example of plain text content inside the accordion component.
1. Number One
2. Number Two
3. Number Three
</Accordion>\n` },
{ label: "Youtube", value: `<Youtube videoId="your-video-id" />\n` },
{ label: "Tooltip", value: `What do you know about <Tooltip text="DocuBook" tip="npx @docubook/create@latest" /> ?\n` },
{ label: "Tabs", value: `<Tabs defaultValue="tab1" className="pt-5 pb-1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">
Content for tab 1
</TabsContent>
<TabsContent value="tab2">
Content for tab 2
</TabsContent>
</Tabs>\n` },
{ label: "Note", value: `<Note type="note" title="Note">
This is a note message.
</Note>\n` },
{ label: "Danger", value: `<Note type="danger" title="Danger">
This is a danger message.
</Note>\n` },
{ label: "Warning", value: `<Note type="warning" title="Warning">
This is a warning message.
</Note>\n` },
{ label: "Success", value: `<Note type="success" title="Success">
This is a success message.
</Note>\n` }
];

View File

@@ -2,7 +2,7 @@
import { ArrowUpIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "../ui/button";
import { Button } from "./ui/button";
import { cn } from "@/lib/utils";
export function ScrollToTop() {

View File

@@ -96,14 +96,14 @@ export default function Search() {
}}
>
<DialogTrigger asChild>
<div className="relative flex-1 cursor-pointer max-w-[160px]">
<div className="relative flex-1 cursor-pointer max-w-[140px]">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-500 dark:text-stone-400" />
<Input
className="md:w-full rounded-md dark:bg-background/95 bg-background border h-9 pl-10 pr-0 sm:pr-4 text-sm shadow-sm overflow-ellipsis"
className="md:w-full rounded-full dark:bg-background/95 bg-background border h-9 pl-10 pr-0 sm:pr-4 text-sm shadow-sm overflow-ellipsis"
placeholder="Search"
type="search"
/>
<div className="flex absolute top-1/2 -translate-y-1/2 right-2 text-xs font-medium font-mono items-center gap-0.5 dark:bg-black dark:border dark:border-white/20 bg-stone-200/50 border border-black/40 p-1 rounded-sm">
<div className="flex absolute top-1/2 -translate-y-1/2 right-2 text-xs font-medium font-mono items-center gap-0.5 dark:bg-accent bg-accent text-white px-2 py-0.5 rounded-full">
<CommandIcon className="w-3 h-3" />
<span>K</span>
</div>

45
components/ui/aurora.tsx Normal file
View File

@@ -0,0 +1,45 @@
"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;

153
components/ui/command.tsx Normal file
View File

@@ -0,0 +1,153 @@
"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,
}

View File

@@ -0,0 +1,324 @@
"use client";
import React, { useEffect, useRef, useState } 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 [rotation, setRotation] = 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>();
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);
const svgString = renderToString(item as React.ReactElement);
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"
/>
);
}

View File

@@ -0,0 +1,35 @@
import React from "react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
interface InteractiveHoverButtonProps
extends 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 />
</div>
</button>
);
});
InteractiveHoverButton.displayName = "InteractiveHoverButton";

View File

@@ -0,0 +1,64 @@
"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;

View File

@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -23,6 +23,7 @@
"@radix-ui/react-toggle-group": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"framer-motion": "^12.4.1",
"geist": "^1.3.1",
"gray-matter": "^4.0.3",

37
pnpm-lock.yaml generated
View File

@@ -50,6 +50,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
cmdk:
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
framer-motion:
specifier: ^12.4.1
version: 12.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1017,6 +1020,12 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cmdk@1.1.1:
resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
collapse-white-space@2.1.0:
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
@@ -3723,6 +3732,18 @@ snapshots:
clsx@2.1.1: {}
cmdk@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-dialog': 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
collapse-white-space@2.1.0: {}
color-convert@2.0.1:
@@ -3960,8 +3981,8 @@ snapshots:
'@typescript-eslint/parser': 8.24.1(eslint@8.57.1)(typescript@5.7.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.4(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
@@ -3980,7 +4001,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1):
eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
@@ -3991,22 +4012,22 @@ snapshots:
stable-hash: 0.0.4
tinyglobby: 0.2.12
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.24.1(eslint@8.57.1)(typescript@5.7.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -4017,7 +4038,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3