update versi docubook
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
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 { useEffect, useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ScrollToTop() {
|
||||
@@ -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
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 }
|
||||
Reference in New Issue
Block a user