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

@@ -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) => {
@@ -114,10 +115,13 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
<DialogTitle className="sr-only">Search Documentation</DialogTitle>
<DialogDescription className="sr-only">Search through the documentation</DialogDescription>
</DialogHeader>
<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

@@ -4,7 +4,7 @@ import { ReactNode, useState, useContext } from 'react';
import { ChevronRight } from 'lucide-react';
import * as Icons from "lucide-react";
import { cn } from '@/lib/utils';
import { AccordionGroupContext } from '@/components/contexts/AccordionContext';
import { AccordionGroupContext } from '@/components/contexts/AccordionContext';
type AccordionProps = {
title: string;
@@ -27,7 +27,7 @@ const Accordion: React.FC<AccordionProps> = ({
// The main wrapper div for the accordion.
// All styling logic for the accordion container is handled here.
return (
<div
<div
className={cn(
// Style for STANDALONE: full card with border & shadow
!isInGroup && "border rounded-lg shadow-sm",
@@ -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"
/>
);
}