chore: Sync package version v2.0.0-beta.1

This commit is contained in:
Bot DocuBook
2026-01-18 13:42:31 +00:00
parent 6ea39676c8
commit 039d8d5a50
24 changed files with 579 additions and 692 deletions

View File

@@ -12,13 +12,19 @@ import MobToc from "@/components/mob-toc";
const { meta } = docuConfig;
type PageProps = {
params: {
params: Promise<{
slug: string[];
};
}>;
};
// Function to generate metadata dynamically
export async function generateMetadata({ params: { slug = [] } }: PageProps) {
export async function generateMetadata(props: PageProps) {
const params = await props.params;
const {
slug = []
} = params;
const pathName = slug.join("/");
const res = await getDocsForSlug(pathName);
@@ -62,7 +68,13 @@ export async function generateMetadata({ params: { slug = [] } }: PageProps) {
};
}
export default async function DocsPage({ params: { slug = [] } }: PageProps) {
export default async function DocsPage(props: PageProps) {
const params = await props.params;
const {
slug = []
} = params;
const pathName = slug.join("/");
const res = await getDocsForSlug(pathName);

View File

@@ -6,6 +6,9 @@ import { GeistMono } from "geist/font/mono";
import { Footer } from "@/components/footer";
import docuConfig from "@/docu.json";
import { Toaster } from "@/components/ui/sonner";
import "@docsearch/css";
import "@/styles/algolia.css";
import "@/styles/syntax.css";
import "@/styles/globals.css";
const { meta } = docuConfig;

View File

@@ -25,21 +25,21 @@ export default function Home() {
)}
>
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-100 hover:duration-300 hover:dark:text-neutral-200">
<span>🚀 New Version - Release v1.16.1</span>
<span>🚀 Release v2.0.0-beta.1</span>
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
</AnimatedShinyText>
</div>
</div>
</Link>
<div className="w-full max-w-[800px] pb-8">
<div className="w-full max-w-[800px] pb-8">
<h1 className="mb-4 text-2xl font-bold sm:text-5xl">DocuBook Starter Templates</h1>
<p className="mb-8 sm:text-xl text-muted-foreground">
Get started by editing app/page.tsx . Save and see your changes instantly.{' '}
<Link className="text-primary underline" href="https://www.docubook.pro/docs/getting-started/introduction" target="_blank">
Get started by editing app/page.tsx . Save and see your changes instantly.{' '}
<Link className="text-primary underline" href="https://www.docubook.pro/docs/getting-started/introduction" target="_blank">
Read Documentations
</Link>
</Link>
</p>
</div>
</div>
<div className="flex flex-row items-center gap-6 mb-10">
<Link
href={`/docs${page_routes[0].href}`}

View File

@@ -50,6 +50,7 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
useEffect(() => {
if (!isOpen) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSearchedInput("");
}
}, [isOpen]);
@@ -71,9 +72,9 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
return advanceSearch(trimmedInput) as unknown as SearchResult[];
}, [searchedInput]);
useEffect(() => {
setSelectedIndex(0);
}, [filteredResults]);
// useEffect(() => {
// setSelectedIndex(0);
// }, [filteredResults]);
useEffect(() => {
const handleNavigation = (event: KeyboardEvent) => {
@@ -117,7 +118,10 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
<input
value={searchedInput}
onChange={(e) => setSearchedInput(e.target.value)}
onChange={(e) => {
setSearchedInput(e.target.value);
setSelectedIndex(0);
}}
placeholder="Type something to search..."
autoFocus
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full"
@@ -138,38 +142,38 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
const isActive = index === selectedIndex;
return (
<DialogClose key={item.href} asChild>
<Anchor
ref={(el) => {
itemRefs.current[index] = el as HTMLDivElement | null;
}}
<DialogClose key={item.href} asChild>
<Anchor
ref={(el) => {
itemRefs.current[index] = el as HTMLDivElement | null;
}}
className={cn(
"dark:hover:bg-accent/15 hover:bg-accent/10 w-full px-3 rounded-sm text-sm flex items-center gap-2.5",
isActive && "bg-primary/20 dark:bg-primary/30",
paddingClass
)}
href={`/docs${item.href}`}
tabIndex={-1}
>
<div
className={cn(
"dark:hover:bg-accent/15 hover:bg-accent/10 w-full px-3 rounded-sm text-sm flex items-center gap-2.5",
isActive && "bg-primary/20 dark:bg-primary/30",
paddingClass
"flex items-center w-full h-full py-3 gap-1.5 px-2 justify-between",
level > 1 && "border-l pl-4"
)}
href={`/docs${item.href}`}
tabIndex={-1}
>
<div
className={cn(
"flex items-center w-full h-full py-3 gap-1.5 px-2 justify-between",
level > 1 && "border-l pl-4"
)}
>
<div className="flex items-center">
<FileTextIcon className="h-[1.1rem] w-[1.1rem] mr-1" />
<span>{item.title}</span>
</div>
{isActive && (
<div className="hidden md:flex items-center text-xs text-muted-foreground">
<span>Return</span>
<CornerDownLeftIcon className="h-3 w-3 ml-1" />
</div>
)}
>
<div className="flex items-center">
<FileTextIcon className="h-[1.1rem] w-[1.1rem] mr-1" />
<span>{item.title}</span>
</div>
</Anchor>
</DialogClose>
{isActive && (
<div className="hidden md:flex items-center text-xs text-muted-foreground">
<span>Return</span>
<CornerDownLeftIcon className="h-3 w-3 ml-1" />
</div>
)}
</div>
</Anchor>
</DialogClose>
);
})}
</div>
@@ -177,14 +181,14 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
<DialogFooter className="md:flex md:justify-start hidden h-14 px-6 bg-transparent border-t text-[14px] outline-none">
<div className="flex items-center gap-2">
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
<ArrowUpIcon className="w-3 h-3"/>
<ArrowUpIcon className="w-3 h-3" />
</span>
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
<ArrowDownIcon className="w-3 h-3"/>
<ArrowDownIcon className="w-3 h-3" />
</span>
<p className="text-muted-foreground">to navigate</p>
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
<CornerDownLeftIcon className="w-3 h-3"/>
<CornerDownLeftIcon className="w-3 h-3" />
</span>
<p className="text-muted-foreground">to select</p>
<span className="dark:bg-accent/15 bg-slate-200 border rounded px-2 py-1">

View File

@@ -45,6 +45,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
useEffect(() => {
if (pathname.startsWith("/docs")) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setActiveRoute(getActiveContextRoute(pathname));
} else {
setActiveRoute(undefined);
@@ -61,7 +62,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
<Button
variant="ghost"
className={cn(
"w-full max-w-[240px] flex items-center justify-between font-semibold text-foreground px-0 pt-8",
"w-full max-w-[240px] cursor-pointer flex items-center justify-between font-semibold text-foreground px-0 pt-8",
"hover:bg-transparent hover:text-foreground",
className
)}

View File

@@ -1,8 +1,7 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
import { type ThemeProviderProps } from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

@@ -29,7 +29,7 @@ export function ToggleButton({
<Button
size="icon"
variant="outline"
className="hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
className="cursor-pointer hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
onClick={onToggle}
>
{collapsed ? (

View File

@@ -38,16 +38,16 @@ const Accordion: React.FC<AccordionProps> = ({
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex items-center space-x-2 w-full px-4 h-12 transition-colors bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70"
className="flex items-center gap-2 w-full px-4 h-12 transition-colors bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70"
>
<ChevronRight
className={cn(
"w-4 h-4 text-muted-foreground transition-transform duration-200",
"w-4 h-4 text-muted-foreground transition-transform duration-200 flex-shrink-0",
isOpen && "rotate-90"
)}
/>
{Icon && <Icon className="text-foreground w-4 h-4"/> }
<h3 className="font-medium text-base text-foreground m-0">{title}</h3>
{Icon && <Icon className="text-foreground w-4 h-4 flex-shrink-0" />}
<h3 className="font-medium text-base text-foreground !m-0 leading-none">{title}</h3>
</button>
{isOpen && (

View File

@@ -8,13 +8,21 @@ interface CardGroupProps {
}
const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className }) => {
const cardsArray = React.Children.toArray(children); // Pastikan children berupa array
const cardsArray = React.Children.toArray(children);
// Static grid column classes for Tailwind v4 compatibility
const gridColsClass = {
1: "grid-cols-1",
2: "grid-cols-1 sm:grid-cols-2",
3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
}[cols] || "grid-cols-1 sm:grid-cols-2";
return (
<div
className={clsx(
"grid gap-4 text-foreground",
`grid-cols-1 sm:grid-cols-${cols}`,
gridColsClass,
className
)}
>

View File

@@ -100,8 +100,8 @@ export const Files = ({ children }: { children: ReactNode }) => {
return (
<div
className="
rounded-xl border border-muted/50
bg-card/50 backdrop-blur-sm
rounded-xl border border-muted/20
bg-card/20 backdrop-blur-sm
shadow-sm overflow-hidden
transition-all duration-200
hover:shadow-md hover:border-muted/60

View File

@@ -4,13 +4,17 @@ import NextImage from "next/image";
type Height = ComponentProps<typeof NextImage>["height"];
type Width = ComponentProps<typeof NextImage>["width"];
type ImageProps = Omit<ComponentProps<"img">, "src"> & {
src?: ComponentProps<typeof NextImage>["src"];
};
export default function Image({
src,
alt = "alt",
width = 800,
height = 350,
...props
}: ComponentProps<"img">) {
}: ImageProps) {
if (!src) return null;
return (
<NextImage

View File

@@ -1,4 +1,4 @@
import { type ComponentProps } from "react";
import { type ComponentProps, type JSX } from "react";
import Copy from "./CopyMdx";
import {
SiJavascript,

View File

@@ -14,7 +14,7 @@ interface MobTocProps {
tocs: TocItem[];
}
const useClickOutside = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
const useClickOutside = (ref: React.RefObject<HTMLElement | null>, callback: () => void) => {
const handleClick = React.useCallback((event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback();

View File

@@ -32,6 +32,7 @@ export function ScrollToTop({
useEffect(() => {
// Initial check
// eslint-disable-next-line react-hooks/set-state-in-effect
checkScroll();
// Set up scroll listener with debounce for better performance

View File

@@ -44,6 +44,7 @@ export default function SubLink({
// Auto-expand if current path is a child of this item
useEffect(() => {
if (items && (path.startsWith(fullHref) && path !== fullHref)) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsOpen(true);
}
}, [path, fullHref, items]);

View File

@@ -1,68 +1,69 @@
"use client";
import * as React from "react";
import { Moon, Sun, Monitor } from "lucide-react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export function ModeToggle() {
const { theme, setTheme } = useTheme();
const [selectedTheme, setSelectedTheme] = React.useState<string>("system");
const { theme, setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = React.useState(false);
// Pastikan toggle tetap di posisi yang benar setelah reload
// Untuk menghindari hydration mismatch
React.useEffect(() => {
if (theme) {
setSelectedTheme(theme);
setMounted(true);
}, []);
// Jika belum mounted, jangan render apapun untuk menghindari mismatch
if (!mounted) {
return (
<div className="flex items-center gap-1 rounded-full border border-border bg-background/50 p-1">
<div className="rounded-full p-1 w-8 h-8" />
<div className="rounded-full p-1 w-8 h-8" />
</div>
);
}
// Tentukan theme yang aktif: gunakan resolvedTheme untuk menampilkan ikon yang sesuai
// jika theme === "system", resolvedTheme akan menjadi "light" atau "dark" sesuai device
const activeTheme = theme === "system" || !theme ? resolvedTheme : theme;
const handleToggle = () => {
// Toggle antara light dan dark
// Jika sekarang light, ganti ke dark, dan sebaliknya
if (activeTheme === "light") {
setTheme("dark");
} else {
setSelectedTheme("system"); // Default ke system jika undefined
setTheme("light");
}
}, [theme]);
};
return (
<ToggleGroup
type="single"
value={selectedTheme}
onValueChange={(value) => {
if (value) {
setTheme(value);
setSelectedTheme(value);
}
}}
value={activeTheme}
onValueChange={handleToggle}
className="flex items-center gap-1 rounded-full border border-border bg-background/50 p-1 transition-all"
>
<ToggleGroupItem
value="light"
size="sm"
aria-label="Light Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "light"
? "bg-primary text-primary-foreground"
: "bg-transparent hover:bg-muted/50"
}`}
className={`rounded-full p-1 transition-all ${activeTheme === "light"
? "bg-primary text-primary-foreground"
: "bg-transparent hover:bg-muted/50"
}`}
>
<Sun className="h-4 w-4" />
</ToggleGroupItem>
<ToggleGroupItem
value="system"
size="sm"
aria-label="System Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "system"
? "bg-primary text-primary-foreground"
: "bg-transparent hover:bg-muted/50"
}`}
>
<Monitor className="h-4 w-4" />
</ToggleGroupItem>
<ToggleGroupItem
value="dark"
size="sm"
aria-label="Dark Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "dark"
? "bg-primary text-primary-foreground"
: "bg-transparent hover:bg-muted/50"
}`}
className={`rounded-full p-1 transition-all ${activeTheme === "dark"
? "bg-primary text-primary-foreground"
: "bg-transparent hover:bg-muted/50"
}`}
>
<Moon className="h-4 w-4" />
</ToggleGroupItem>

View File

@@ -1,324 +0,0 @@
"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] = 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
eslint.config.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { defineConfig } from "eslint/config";
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default defineConfig([{
extends: [
...nextCoreWebVitals,
...nextTypescript,
...compat.extends("plugin:@typescript-eslint/recommended")
],
rules: {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
}],
"@typescript-eslint/no-empty-object-type": "off",
},
}]);

View File

@@ -16,6 +16,7 @@ export function useScrollPosition(threshold = 0.5) {
// Add scroll event listener
useEffect(() => {
// Initial check
// eslint-disable-next-line react-hooks/set-state-in-effect
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });

View File

@@ -1,62 +1,67 @@
{
"name": "docubook",
"version": "1.16.1",
"version": "2.0.0-beta.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "eslint ."
},
"dependencies": {
"@docsearch/css": "3",
"@docsearch/css": "^3.9.0",
"@docsearch/react": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"algoliasearch": "^5.35.0",
"class-variance-authority": "^0.7.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"algoliasearch": "^5.46.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"framer-motion": "^12.4.1",
"geist": "^1.3.1",
"cmdk": "^1.1.1",
"framer-motion": "^12.26.2",
"geist": "^1.5.1",
"gray-matter": "^4.0.3",
"lucide-react": "^0.511.0",
"next": "^14.2.6",
"next": "16.1.3",
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"next-themes": "^0.4.4",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-icons": "^5.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-code-titles": "^1.2.0",
"rehype-prism-plus": "^2.0.0",
"rehype-code-titles": "^1.2.1",
"rehype-prism-plus": "^2.0.1",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0",
"sonner": "^1.4.3",
"tailwind-merge": "^2.5.2",
"remark-gfm": "^4.0.1",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.14",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.20",
"eslint": "^8",
"eslint-config-next": "^14.2.6",
"postcss": "^8",
"tailwindcss": "^3.4.10",
"typescript": "^5"
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^20.19.30",
"@types/react": "19.2.8",
"@types/react-dom": "19.2.3",
"autoprefixer": "^10.4.23",
"eslint": "^9.39.2",
"eslint-config-next": "16.1.3",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
},
"overrides": {
"@types/react": "19.2.8",
"@types/react-dom": "19.2.3"
}
}

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
'@tailwindcss/postcss': {},
autoprefixer: {},
},
};

View File

@@ -1,69 +1,189 @@
@import "@docsearch/css";
@import "./algolia.css";
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("./syntax.css");
@custom-variant dark (&:is(.dark *));
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >=--theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >=1440px) {
max-width: 1440px;
}
}
@theme {
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-sidebar: hsl(var(--sidebar-background));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--font-code: var(--font-geist-mono);
--font-regular: var(--font-geist-sans);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-shiny-text: shiny-text 8s infinite;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes shiny-text {
0%,
90%,
100% {
background-position: calc(-100% - var(--shiny-width)) 0;
}
30%,
60% {
background-position: calc(100% + var(--shiny-width)) 0;
}
}
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@utility animate-shine {
--animate-shine: shine var(--duration) infinite linear;
animation: var(--animate-shine);
background-size: 200% 200%;
}
/* Modern Blue Theme */
@layer base {
:root {
--background: 210 40% 98%;
--foreground: 220 30% 15%;
--card: 0 0% 100%;
--card-foreground: 220 30% 15%;
--popover: 0 0% 100%;
--popover-foreground: 220 30% 15%;
--primary: 210 81% 56%; /* #2281E3 */
--primary-foreground: 0 0% 100%;
--secondary: 210 30% 90%;
--secondary-foreground: 220 30% 15%;
--muted: 210 20% 92%;
--muted-foreground: 220 15% 50%;
--accent: 200 100% 40%;
--accent-foreground: 0 0% 100%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 100%;
--border: 210 20% 85%;
--input: 210 20% 85%;
--ring: 210 81% 56%;
--radius: 0.5rem;
--chart-1: 210 81% 56%;
--chart-2: 200 100% 40%;
--chart-3: 220 76% 60%;
--chart-4: 190 90% 50%;
--chart-5: 230 86% 45%;
--line-number-color: rgba(0, 0, 0, 0.05);
}
:root {
--background: 210 40% 98%;
--foreground: 220 30% 15%;
--card: 0 0% 100%;
--card-foreground: 220 30% 15%;
--popover: 0 0% 100%;
--popover-foreground: 220 30% 15%;
--primary: 210 81% 56%;
/* #2281E3 */
--primary-foreground: 0 0% 100%;
--secondary: 210 30% 90%;
--secondary-foreground: 220 30% 15%;
--muted: 210 20% 92%;
--muted-foreground: 220 15% 50%;
--accent: 200 100% 40%;
--accent-foreground: 0 0% 100%;
--destructive: 0 85% 60%;
--destructive-foreground: 0 0% 100%;
--border: 210 20% 85%;
--input: 210 20% 85%;
--ring: 210 81% 56%;
--radius: 0.5rem;
--chart-1: 210 81% 56%;
--chart-2: 200 100% 40%;
--chart-3: 220 76% 60%;
--chart-4: 190 90% 50%;
--chart-5: 230 86% 45%;
--line-number-color: rgba(0, 0, 0, 0.05);
}
.dark {
--background: 220 25% 10%;
--foreground: 210 30% 96%;
--card: 220 25% 15%;
--card-foreground: 210 30% 96%;
--popover: 220 25% 15%;
--popover-foreground: 210 30% 96%;
--primary: 210 100% 65%;
--primary-foreground: 220 25% 10%;
--secondary: 215 25% 20%;
--secondary-foreground: 210 30% 96%;
--muted: 215 20% 25%;
--muted-foreground: 215 20% 65%;
--accent: 200 100% 60%;
--accent-foreground: 0 0% 100%;
--destructive: 0 85% 70%;
--destructive-foreground: 0 0% 100%;
--border: 215 20% 25%;
--input: 215 20% 25%;
--ring: 210 100% 65%;
--chart-1: 210 100% 65%;
--chart-2: 200 100% 60%;
--chart-3: 220 90% 70%;
--chart-4: 190 100% 65%;
--chart-5: 230 90% 60%;
--line-number-color: rgba(255, 255, 255, 0.1);
}
.dark {
--background: 220 25% 10%;
--foreground: 210 30% 96%;
--card: 220 25% 15%;
--card-foreground: 210 30% 96%;
--popover: 220 25% 15%;
--popover-foreground: 210 30% 96%;
--primary: 210 100% 65%;
--primary-foreground: 220 25% 10%;
--secondary: 215 25% 20%;
--secondary-foreground: 210 30% 96%;
--muted: 215 20% 25%;
--muted-foreground: 215 20% 65%;
--accent: 200 100% 60%;
--accent-foreground: 0 0% 100%;
--destructive: 0 85% 70%;
--destructive-foreground: 0 0% 100%;
--border: 215 20% 25%;
--input: 215 20% 25%;
--ring: 210 100% 65%;
--chart-1: 210 100% 65%;
--chart-2: 200 100% 60%;
--chart-3: 220 90% 70%;
--chart-4: 190 100% 65%;
--chart-5: 230 90% 60%;
--line-number-color: rgba(255, 255, 255, 0.1);
}
}
@layer base {
@@ -76,67 +196,66 @@
}
}
.prose {
margin: 0 !important;
}
@layer utilities {
.prose {
margin: 0 !important;
}
pre {
padding: 2px 0 !important;
width: inherit !important;
overflow-x: auto;
}
pre {
padding: 2px 0 !important;
width: inherit !important;
overflow-x: auto;
}
pre>code {
display: grid;
max-width: inherit !important;
padding: 14px 0 !important;
border: 0 !important;
}
pre>code {
display: grid;
max-width: inherit !important;
padding: 14px 0 !important;
border: 0 !important;
}
.code-line {
padding: 0.75px 16px;
@apply border-l-2 border-transparent
}
.code-line {
padding: 0.75px 16px;
@apply border-l-2 border-transparent;
}
.line-number::before {
display: inline-block;
width: 1rem;
margin-right: 22px;
margin-left: -2px;
color: rgb(110, 110, 110);
content: attr(line);
font-size: 13.5px;
text-align: right;
}
.line-number::before {
display: inline-block;
width: 1rem;
margin-right: 22px;
margin-left: -2px;
color: rgb(110, 110, 110);
content: attr(line);
font-size: 13.5px;
text-align: right;
}
.highlight-line {
@apply bg-primary/5 border-l-2 border-primary/30;
}
.highlight-line {
@apply bg-primary/5 border-l-2 border-primary/30;
}
.rehype-code-title {
@apply px-2 -mb-8 w-full text-sm pb-5 font-medium mt-5 font-code;
}
.rehype-code-title {
@apply px-2 -mb-8 w-full text-sm pb-5 font-medium mt-5 font-code;
}
.highlight-comp>code {
background-color: transparent !important;
.highlight-comp>code {
background-color: transparent !important;
}
}
@layer utilities {
.animate-shine {
--animate-shine: shine var(--duration) infinite linear;
animation: var(--animate-shine);
background-size: 200% 200%;
@keyframes shine {
0% {
background-position: 0% 0%;
}
@keyframes shine {
0% {
background-position: 0% 0%;
}
50% {
background-position: 100% 100%;
}
100% {
background-position: 0% 0%;
}
50% {
background-position: 100% 100%;
}
100% {
background-position: 0% 0%;
}
}
}

View File

@@ -1,111 +1,113 @@
import type { Config } from "tailwindcss";
import tailwindAnimate from "tailwindcss-animate";
import typography from "@tailwindcss/typography";
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1440px'
}
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
code: ["var(--font-geist-mono)"],
regular: ["var(--font-geist-sans)"]
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
},
'shiny-text': {
'0%, 90%, 100%': {
'background-position': 'calc(-100% - var(--shiny-width)) 0'
},
'30%, 60%': {
'background-position': 'calc(100% + var(--shiny-width)) 0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'shiny-text': 'shiny-text 8s infinite'
}
}
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
darkMode: "class",
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1440px'
}
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
code: ["var(--font-geist-mono)"],
regular: ["var(--font-geist-sans)"]
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
},
'shiny-text': {
'0%, 90%, 100%': {
'background-position': 'calc(-100% - var(--shiny-width)) 0'
},
'30%, 60%': {
'background-position': 'calc(100% + var(--shiny-width)) 0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'shiny-text': 'shiny-text 8s infinite'
}
}
},
plugins: [tailwindAnimate, typography],
} satisfies Config;
export default config;

View File

@@ -1,6 +1,10 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -10,7 +14,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -18,9 +22,20 @@
}
],
"paths": {
"@/*": ["./*"]
}
"@/*": [
"./*"
]
},
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}