update versi docubook
This commit is contained in:
@@ -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.
|
> **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.
|
||||||
|
|
||||||
[](https://vercel.com/import/project?template=https://github.com/mywildancloud/docubook)
|
[](https://vercel.com/import/project?template=https://github.com/gitfromwildan/docubook)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default async function BlogIndexPage() {
|
|||||||
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
|
(a, b) => stringToDate(b.date).getTime() - stringToDate(a.date).getTime()
|
||||||
);
|
);
|
||||||
return (
|
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">
|
<div className="mb-7 flex flex-col gap-2">
|
||||||
<h1 className="text-2xl font-extrabold">
|
<h1 className="text-2xl font-extrabold">
|
||||||
Blog Posts
|
Blog Posts
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { getMetadata } from "@/app/layout";
|
|
||||||
|
|
||||||
export const metadata = getMetadata({
|
|
||||||
title: "Hire Me",
|
|
||||||
description: "Hire me to start a documentation project with DocuBook",
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function EmbeddedHTML() {
|
|
||||||
return (
|
|
||||||
<div className="w-full py-0 dark:bg-transparent mx-auto min-h-svh">
|
|
||||||
<iframe
|
|
||||||
src="/hire-me.html"
|
|
||||||
width="100%"
|
|
||||||
height="1000"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,8 @@ import {
|
|||||||
Laptop2,
|
Laptop2,
|
||||||
Copy,
|
Copy,
|
||||||
Download,
|
Download,
|
||||||
RotateCcw
|
RotateCcw,
|
||||||
|
Calendar
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button as UIButton } from "@/components/ui/button";
|
import { Button as UIButton } from "@/components/ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -38,8 +39,23 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuSeparator,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
} 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";
|
import "@/styles/editor.css";
|
||||||
|
|
||||||
const ToolbarButton = ({ icon: Icon, label, onClick }: { icon: any, label: string, onClick?: () => void }) => (
|
const ToolbarButton = ({ icon: Icon, label, onClick }: { icon: any, label: string, onClick?: () => void }) => (
|
||||||
@@ -167,9 +183,7 @@ export default function PlaygroundPage() {
|
|||||||
</UIButton>
|
</UIButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
), {
|
), { duration: 10000 });
|
||||||
duration: 10000,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,149 +193,17 @@ export default function PlaygroundPage() {
|
|||||||
const before = markdown.substring(0, start);
|
const before = markdown.substring(0, start);
|
||||||
const after = markdown.substring(end);
|
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);
|
setMarkdown(newText);
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
textArea.focus();
|
textArea.focus();
|
||||||
const newPosition = start + text.length;
|
const newPosition = start + text.length + 1;
|
||||||
textArea.setSelectionRange(newPosition, newPosition);
|
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, '\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) {
|
if (isMobile) {
|
||||||
return <MobileMessage />;
|
return <MobileMessage />;
|
||||||
@@ -402,86 +284,88 @@ export default function PlaygroundPage() {
|
|||||||
</UIButton>
|
</UIButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center border-b p-1 bg-background">
|
<div className="flex items-center border-b p-1 bg-background">
|
||||||
<ToolbarButton icon={Type} label="Paragraph" onClick={handleParagraphClick} />
|
<ToolbarButton icon={Calendar} label="Metadata" onClick={() => handleMetadataClick(insertAtCursor)} />
|
||||||
<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} />
|
|
||||||
<ToolbarSeparator />
|
<ToolbarSeparator />
|
||||||
<ToolbarButton icon={Code} label="Code Block" onClick={handleCodeBlockClick} />
|
<ToolbarButton icon={Type} label="Paragraph" onClick={() => handleParagraphClick(insertAtCursor)} />
|
||||||
<ToolbarButton icon={Quote} label="Blockquote" onClick={handleBlockquoteClick} />
|
<ToolbarButton icon={Heading2} label="Heading 2" onClick={() => handleHeading2Click(insertAtCursor)} />
|
||||||
<ToolbarButton icon={ImageIcon} label="Image" onClick={handleImageClick} />
|
<ToolbarButton icon={Heading3} label="Heading 3" onClick={() => handleHeading3Click(insertAtCursor)} />
|
||||||
<ToolbarButton icon={LinkIcon} label="Link" onClick={handleLinkClick} />
|
<ToolbarButton icon={List} label="Bullet List" onClick={() => handleBulletListClick(insertAtCursor)} />
|
||||||
<ToolbarButton icon={Table} label="Table" onClick={handleTableClick} />
|
<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 />
|
<ToolbarSeparator />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<UIButton
|
<UIButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 px-2 flex items-center gap-1 font-normal"
|
className="h-8 px-2 flex items-center gap-1 font-normal"
|
||||||
>
|
>
|
||||||
<Notebook className="h-4 w-4 text-muted-foreground" />
|
<Notebook className="h-4 w-4" />
|
||||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</UIButton>
|
</UIButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuItem onClick={() => handleNoteClick('note')}>
|
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'note')}>
|
||||||
Note
|
Note
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleNoteClick('danger')}>
|
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'danger')}>
|
||||||
Danger
|
Danger
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleNoteClick('warning')}>
|
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'warning')}>
|
||||||
Warning
|
Warning
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleNoteClick('success')}>
|
<DropdownMenuItem onClick={handleNoteClick(insertAtCursor, 'success')}>
|
||||||
Success
|
Success
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<ToolbarSeparator />
|
<ToolbarSeparator />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<UIButton
|
<UIButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 px-2 flex items-center gap-1 font-normal"
|
className="h-8 px-2 flex items-center gap-1 font-normal"
|
||||||
>
|
>
|
||||||
<Component className="h-4 w-4 text-muted-foreground" />
|
<Component className="h-4 w-4" />
|
||||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</UIButton>
|
</UIButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
<DropdownMenuItem onClick={() => handleComponentClick('stepper')}>
|
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'stepper')}>
|
||||||
<Rows className="h-4 w-4 mr-2" />
|
<Rows className="h-4 w-4 mr-2" />
|
||||||
Stepper
|
Stepper
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleComponentClick('card')}>
|
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'card')}>
|
||||||
<LayoutGrid className="h-4 w-4 mr-2" />
|
<LayoutGrid className="h-4 w-4 mr-2" />
|
||||||
Card
|
Card
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleComponentClick('button')}>
|
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'button')}>
|
||||||
<MousePointer2 className="h-4 w-4 mr-2" />
|
<MousePointer2 className="h-4 w-4 mr-2" />
|
||||||
Button
|
Button
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleComponentClick('accordion')}>
|
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'accordion')}>
|
||||||
<ChevronDown className="h-4 w-4 mr-2" />
|
<ChevronDown className="h-4 w-4 mr-2" />
|
||||||
Accordion
|
Accordion
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleComponentClick('tabs')}>
|
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'tabs')}>
|
||||||
<LayoutPanelTop className="h-4 w-4 mr-2" />
|
<LayoutPanelTop className="h-4 w-4 mr-2" />
|
||||||
Tabs
|
Tabs
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleComponentClick('youtube')}>
|
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'youtube')}>
|
||||||
<YoutubeIcon className="h-4 w-4 mr-2" />
|
<YoutubeIcon className="h-4 w-4 mr-2" />
|
||||||
Youtube
|
Youtube
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleComponentClick('tooltip')}>
|
<DropdownMenuItem onClick={handleComponentClick(insertAtCursor, 'tooltip')}>
|
||||||
<HelpCircle className="h-4 w-4 mr-2" />
|
<HelpCircle className="h-4 w-4 mr-2" />
|
||||||
Tooltip
|
Tooltip
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -494,13 +378,13 @@ export default function PlaygroundPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={markdown}
|
value={markdown}
|
||||||
onChange={(e) => setMarkdown(e.target.value)}
|
onChange={(e) => setMarkdown(e.target.value)}
|
||||||
className="editor-textarea"
|
className="editor-textarea"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder="Start writing markdown..."
|
placeholder="Type '/' for commands..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { Logo, NavMenu } from "./navbar";
|
import { Logo, NavMenu } from "./navbar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { AlignLeftIcon } from "lucide-react";
|
import { AlignLeftIcon } from "lucide-react";
|
||||||
import { FooterButtons } from "@/components/footer";
|
import { FooterButtons } from "./footer";
|
||||||
import { DialogTitle } from "@/components/ui/dialog";
|
import { DialogTitle } from "./ui/dialog";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import DocsMenu from "@/components/docs-menu";
|
import DocsMenu from "./docs-menu";
|
||||||
import { ModeToggle } from "@/components/theme-toggle";
|
import { ModeToggle } from "./theme-toggle";
|
||||||
|
|
||||||
export function Leftbar() {
|
export function Leftbar() {
|
||||||
return (
|
return (
|
||||||
@@ -46,9 +46,9 @@ export function SheetLeftbar() {
|
|||||||
<div className="mx-2 px-5">
|
<div className="mx-2 px-5">
|
||||||
<DocsMenu isSheet />
|
<DocsMenu isSheet />
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="px-6 py-2 flex justify-start items-center gap-6">
|
<div className="px-6 py-2 flex justify-start items-center gap-6">
|
||||||
<FooterButtons />
|
<FooterButtons />
|
||||||
</div> */}
|
</div>
|
||||||
<div className="flex w-2/4 px-5">
|
<div className="flex w-2/4 px-5">
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Search from "@/components/search";
|
|||||||
import Anchor from "@/components/anchor";
|
import Anchor from "@/components/anchor";
|
||||||
import { SheetLeftbar } from "@/components/leftbar";
|
import { SheetLeftbar } from "@/components/leftbar";
|
||||||
import { SheetClose } from "@/components/ui/sheet";
|
import { SheetClose } from "@/components/ui/sheet";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import docuConfig from "@/docu.json"; // Import JSON
|
import docuConfig from "@/docu.json"; // Import JSON
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
@@ -18,13 +19,13 @@ export function Navbar() {
|
|||||||
<div className="hidden sm:flex">
|
<div className="hidden sm:flex">
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</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">
|
<div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Separator className="hidden sm:flex my-4 h-9" orientation="vertical" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Search />
|
<Search />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,7 +37,7 @@ export function Logo() {
|
|||||||
const { navbar } = docuConfig; // Extract navbar from JSON
|
const { navbar } = docuConfig; // Extract navbar from JSON
|
||||||
|
|
||||||
return (
|
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" />
|
<Image src={navbar.logo.src} alt={navbar.logo.alt} width="24" height="24" />
|
||||||
<h2 className="font-bold font-code text-md">{navbar.logoText}</h2>
|
<h2 className="font-bold font-code text-md">{navbar.logoText}</h2>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getPreviousNext } from "@/lib/markdown";
|
import { getPreviousNext } from "@/lib/markdown";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "./ui/button";
|
||||||
|
|
||||||
export default function Pagination({ pathname }: { pathname: string }) {
|
export default function Pagination({ pathname }: { pathname: string }) {
|
||||||
const res = getPreviousNext(pathname);
|
const res = getPreviousNext(pathname);
|
||||||
|
|||||||
252
components/playground/MarkComponent.tsx
Normal file
252
components/playground/MarkComponent.tsx
Normal 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, "\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: "\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` }
|
||||||
|
|
||||||
|
];
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { ArrowUpIcon } from "lucide-react";
|
import { ArrowUpIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function ScrollToTop() {
|
export function ScrollToTop() {
|
||||||
|
|||||||
@@ -96,14 +96,14 @@ export default function Search() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<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" />
|
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-500 dark:text-stone-400" />
|
||||||
<Input
|
<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"
|
placeholder="Search"
|
||||||
type="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" />
|
<CommandIcon className="w-3 h-3" />
|
||||||
<span>K</span>
|
<span>K</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
45
components/ui/aurora.tsx
Normal file
45
components/ui/aurora.tsx
Normal 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
153
components/ui/command.tsx
Normal 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,
|
||||||
|
}
|
||||||
324
components/ui/icon-cloud.tsx
Normal file
324
components/ui/icon-cloud.tsx
Normal 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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
components/ui/interactive-hover-button.tsx
Normal file
35
components/ui/interactive-hover-button.tsx
Normal 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";
|
||||||
64
components/ui/shine-border.tsx
Normal file
64
components/ui/shine-border.tsx
Normal 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;
|
||||||
15
components/ui/skeleton.tsx
Normal file
15
components/ui/skeleton.tsx
Normal 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 }
|
||||||
119
components/ui/terminal.tsx
Normal file
119
components/ui/terminal.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { motion, MotionProps } from "framer-motion";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface AnimatedSpanProps extends MotionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
delay?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnimatedSpan = ({
|
||||||
|
children,
|
||||||
|
delay = 0,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: AnimatedSpanProps) => (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3, delay: delay / 1000 }}
|
||||||
|
className={cn("grid text-sm font-normal tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface TypingAnimationProps extends MotionProps {
|
||||||
|
children: string;
|
||||||
|
className?: string;
|
||||||
|
duration?: number;
|
||||||
|
delay?: number;
|
||||||
|
as?: React.ElementType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TypingAnimation = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
duration = 60,
|
||||||
|
delay = 0,
|
||||||
|
as: Component = "span",
|
||||||
|
...props
|
||||||
|
}: TypingAnimationProps) => {
|
||||||
|
if (typeof children !== "string") {
|
||||||
|
throw new Error("TypingAnimation: children must be a string. Received:");
|
||||||
|
}
|
||||||
|
|
||||||
|
const MotionComponent = motion.create(Component, {
|
||||||
|
forwardMotionProps: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [displayedText, setDisplayedText] = useState<string>("");
|
||||||
|
const [started, setStarted] = useState(false);
|
||||||
|
const elementRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const startTimeout = setTimeout(() => {
|
||||||
|
setStarted(true);
|
||||||
|
}, delay);
|
||||||
|
return () => clearTimeout(startTimeout);
|
||||||
|
}, [delay]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!started) return;
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const typingEffect = setInterval(() => {
|
||||||
|
if (i < children.length) {
|
||||||
|
setDisplayedText(children.substring(0, i + 1));
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
clearInterval(typingEffect);
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(typingEffect);
|
||||||
|
};
|
||||||
|
}, [children, duration, started]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionComponent
|
||||||
|
ref={elementRef}
|
||||||
|
className={cn("text-sm font-normal tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{displayedText}
|
||||||
|
</MotionComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TerminalProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Terminal = ({ children, className }: TerminalProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"z-0 h-full max-h-[600px] w-full max-w-[640px] rounded-xl border border-border bg-backgroun",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-y-2 border-b border-border p-4">
|
||||||
|
<div className="flex flex-row gap-x-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-red-500"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="p-8">
|
||||||
|
<code className="grid gap-y-1 overflow-auto">{children}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -45,7 +45,7 @@ date : 15-04-2025
|
|||||||
</StepperItem>
|
</StepperItem>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
|
||||||
## Tutorial Video
|
## Video Tutorial
|
||||||
|
|
||||||
<Accordion title="API Sandbox" defaultOpen={true}>
|
<Accordion title="API Sandbox" defaultOpen={true}>
|
||||||
<Youtube videoId="KzD4FxFDKE0" />
|
<Youtube videoId="KzD4FxFDKE0" />
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"framer-motion": "^12.4.1",
|
"framer-motion": "^12.4.1",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
|||||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -50,6 +50,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 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:
|
framer-motion:
|
||||||
specifier: ^12.4.1
|
specifier: ^12.4.1
|
||||||
version: 12.4.7(react-dom@18.3.1(react@18.3.1))(react@18.3.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==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
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:
|
collapse-white-space@2.1.0:
|
||||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||||
|
|
||||||
@@ -3723,6 +3732,18 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
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: {}
|
collapse-white-space@2.1.0: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
|
|||||||
Reference in New Issue
Block a user