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; const { meta } = docuConfig;
type PageProps = { type PageProps = {
params: { params: Promise<{
slug: string[]; slug: string[];
}; }>;
}; };
// Function to generate metadata dynamically // 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 pathName = slug.join("/");
const res = await getDocsForSlug(pathName); 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 pathName = slug.join("/");
const res = await getDocsForSlug(pathName); const res = await getDocsForSlug(pathName);

View File

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

View File

@@ -25,7 +25,7 @@ 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"> <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" /> <ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
</AnimatedShinyText> </AnimatedShinyText>
</div> </div>

View File

@@ -50,6 +50,7 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSearchedInput(""); setSearchedInput("");
} }
}, [isOpen]); }, [isOpen]);
@@ -71,9 +72,9 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
return advanceSearch(trimmedInput) as unknown as SearchResult[]; return advanceSearch(trimmedInput) as unknown as SearchResult[];
}, [searchedInput]); }, [searchedInput]);
useEffect(() => { // useEffect(() => {
setSelectedIndex(0); // setSelectedIndex(0);
}, [filteredResults]); // }, [filteredResults]);
useEffect(() => { useEffect(() => {
const handleNavigation = (event: KeyboardEvent) => { const handleNavigation = (event: KeyboardEvent) => {
@@ -117,7 +118,10 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
<input <input
value={searchedInput} value={searchedInput}
onChange={(e) => setSearchedInput(e.target.value)} onChange={(e) => {
setSearchedInput(e.target.value);
setSelectedIndex(0);
}}
placeholder="Type something to search..." placeholder="Type something to search..."
autoFocus autoFocus
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full" className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full"
@@ -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"> <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"> <div className="flex items-center gap-2">
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-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>
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2"> <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> </span>
<p className="text-muted-foreground">to navigate</p> <p className="text-muted-foreground">to navigate</p>
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2"> <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> </span>
<p className="text-muted-foreground">to select</p> <p className="text-muted-foreground">to select</p>
<span className="dark:bg-accent/15 bg-slate-200 border rounded px-2 py-1"> <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(() => { useEffect(() => {
if (pathname.startsWith("/docs")) { if (pathname.startsWith("/docs")) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setActiveRoute(getActiveContextRoute(pathname)); setActiveRoute(getActiveContextRoute(pathname));
} else { } else {
setActiveRoute(undefined); setActiveRoute(undefined);
@@ -61,7 +62,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
<Button <Button
variant="ghost" variant="ghost"
className={cn( 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", "hover:bg-transparent hover:text-foreground",
className className
)} )}

View File

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

View File

@@ -29,7 +29,7 @@ export function ToggleButton({
<Button <Button
size="icon" size="icon"
variant="outline" 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} onClick={onToggle}
> >
{collapsed ? ( {collapsed ? (

View File

@@ -38,16 +38,16 @@ const Accordion: React.FC<AccordionProps> = ({
<button <button
type="button" type="button"
onClick={() => setIsOpen(!isOpen)} 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 <ChevronRight
className={cn( 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" isOpen && "rotate-90"
)} )}
/> />
{Icon && <Icon className="text-foreground w-4 h-4"/> } {Icon && <Icon className="text-foreground w-4 h-4 flex-shrink-0" />}
<h3 className="font-medium text-base text-foreground m-0">{title}</h3> <h3 className="font-medium text-base text-foreground !m-0 leading-none">{title}</h3>
</button> </button>
{isOpen && ( {isOpen && (

View File

@@ -8,13 +8,21 @@ interface CardGroupProps {
} }
const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className }) => { 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 ( return (
<div <div
className={clsx( className={clsx(
"grid gap-4 text-foreground", "grid gap-4 text-foreground",
`grid-cols-1 sm:grid-cols-${cols}`, gridColsClass,
className className
)} )}
> >

View File

@@ -100,8 +100,8 @@ export const Files = ({ children }: { children: ReactNode }) => {
return ( return (
<div <div
className=" className="
rounded-xl border border-muted/50 rounded-xl border border-muted/20
bg-card/50 backdrop-blur-sm bg-card/20 backdrop-blur-sm
shadow-sm overflow-hidden shadow-sm overflow-hidden
transition-all duration-200 transition-all duration-200
hover:shadow-md hover:border-muted/60 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 Height = ComponentProps<typeof NextImage>["height"];
type Width = ComponentProps<typeof NextImage>["width"]; type Width = ComponentProps<typeof NextImage>["width"];
type ImageProps = Omit<ComponentProps<"img">, "src"> & {
src?: ComponentProps<typeof NextImage>["src"];
};
export default function Image({ export default function Image({
src, src,
alt = "alt", alt = "alt",
width = 800, width = 800,
height = 350, height = 350,
...props ...props
}: ComponentProps<"img">) { }: ImageProps) {
if (!src) return null; if (!src) return null;
return ( return (
<NextImage <NextImage

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ export function ScrollToTop({
useEffect(() => { useEffect(() => {
// Initial check // Initial check
// eslint-disable-next-line react-hooks/set-state-in-effect
checkScroll(); checkScroll();
// Set up scroll listener with debounce for better performance // 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 // Auto-expand if current path is a child of this item
useEffect(() => { useEffect(() => {
if (items && (path.startsWith(fullHref) && path !== fullHref)) { if (items && (path.startsWith(fullHref) && path !== fullHref)) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsOpen(true); setIsOpen(true);
} }
}, [path, fullHref, items]); }, [path, fullHref, items]);

View File

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

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 // Add scroll event listener
useEffect(() => { useEffect(() => {
// Initial check // Initial check
// eslint-disable-next-line react-hooks/set-state-in-effect
handleScroll(); handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('scroll', handleScroll, { passive: true });

View File

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

View File

@@ -1,11 +1,130 @@
@import "@docsearch/css"; @import 'tailwindcss';
@import "./algolia.css"; @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 */ /* Modern Blue Theme */
@layer base { @layer base {
:root { :root {
@@ -15,7 +134,8 @@
--card-foreground: 220 30% 15%; --card-foreground: 220 30% 15%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 220 30% 15%; --popover-foreground: 220 30% 15%;
--primary: 210 81% 56%; /* #2281E3 */ --primary: 210 81% 56%;
/* #2281E3 */
--primary-foreground: 0 0% 100%; --primary-foreground: 0 0% 100%;
--secondary: 210 30% 90%; --secondary: 210 30% 90%;
--secondary-foreground: 220 30% 15%; --secondary-foreground: 220 30% 15%;
@@ -76,29 +196,30 @@
} }
} }
.prose { @layer utilities {
.prose {
margin: 0 !important; margin: 0 !important;
} }
pre { pre {
padding: 2px 0 !important; padding: 2px 0 !important;
width: inherit !important; width: inherit !important;
overflow-x: auto; overflow-x: auto;
} }
pre>code { pre>code {
display: grid; display: grid;
max-width: inherit !important; max-width: inherit !important;
padding: 14px 0 !important; padding: 14px 0 !important;
border: 0 !important; border: 0 !important;
} }
.code-line { .code-line {
padding: 0.75px 16px; padding: 0.75px 16px;
@apply border-l-2 border-transparent @apply border-l-2 border-transparent;
} }
.line-number::before { .line-number::before {
display: inline-block; display: inline-block;
width: 1rem; width: 1rem;
margin-right: 22px; margin-right: 22px;
@@ -107,36 +228,34 @@ pre>code {
content: attr(line); content: attr(line);
font-size: 13.5px; font-size: 13.5px;
text-align: right; text-align: right;
} }
.highlight-line { .highlight-line {
@apply bg-primary/5 border-l-2 border-primary/30; @apply bg-primary/5 border-l-2 border-primary/30;
} }
.rehype-code-title { .rehype-code-title {
@apply px-2 -mb-8 w-full text-sm pb-5 font-medium mt-5 font-code; @apply px-2 -mb-8 w-full text-sm pb-5 font-medium mt-5 font-code;
} }
.highlight-comp>code { .highlight-comp>code {
background-color: transparent !important; background-color: transparent !important;
}
} }
@layer utilities { @layer utilities {
.animate-shine {
--animate-shine: shine var(--duration) infinite linear;
animation: var(--animate-shine);
background-size: 200% 200%;
}
@keyframes shine { @keyframes shine {
0% { 0% {
background-position: 0% 0%; background-position: 0% 0%;
} }
50% { 50% {
background-position: 100% 100%; background-position: 100% 100%;
} }
100% { 100% {
background-position: 0% 0%; background-position: 0% 0%;
} }
} }
} }

View File

@@ -1,7 +1,9 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import tailwindAnimate from "tailwindcss-animate";
import typography from "@tailwindcss/typography";
const config = { const config = {
darkMode: ["class"], darkMode: "class",
content: [ content: [
"./pages/**/*.{ts,tsx}", "./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}",
@@ -105,7 +107,7 @@ const config = {
} }
} }
}, },
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], plugins: [tailwindAnimate, typography],
} satisfies Config; } satisfies Config;
export default config; export default config;

View File

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