Merge pull request #14 from DocuBook/dev-36d3a93
chore: Sync package version v2.0.0-beta.1
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ComponentProps } from "react";
|
||||
import { type ComponentProps, type JSX } from "react";
|
||||
import Copy from "./CopyMdx";
|
||||
import {
|
||||
SiJavascript,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -1,65 +1,66 @@
|
||||
"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);
|
||||
} else {
|
||||
setSelectedTheme("system"); // Default ke system jika undefined
|
||||
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>
|
||||
);
|
||||
}
|
||||
}, [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 (
|
||||
<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"
|
||||
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"
|
||||
className={`rounded-full p-1 transition-all ${activeTheme === "dark"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-transparent hover:bg-muted/50"
|
||||
}`}
|
||||
|
||||
@@ -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
35
eslint.config.mjs
Normal 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",
|
||||
},
|
||||
}]);
|
||||
@@ -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 });
|
||||
|
||||
83
package.json
83
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,130 @@
|
||||
@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 {
|
||||
@@ -15,7 +134,8 @@
|
||||
--card-foreground: 220 30% 15%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 220 30% 15%;
|
||||
--primary: 210 81% 56%; /* #2281E3 */
|
||||
--primary: 210 81% 56%;
|
||||
/* #2281E3 */
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 210 30% 90%;
|
||||
--secondary-foreground: 220 30% 15%;
|
||||
@@ -76,29 +196,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.prose {
|
||||
@layer utilities {
|
||||
.prose {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
pre {
|
||||
padding: 2px 0 !important;
|
||||
width: inherit !important;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
pre>code {
|
||||
pre>code {
|
||||
display: grid;
|
||||
max-width: inherit !important;
|
||||
padding: 14px 0 !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.code-line {
|
||||
.code-line {
|
||||
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;
|
||||
width: 1rem;
|
||||
margin-right: 22px;
|
||||
@@ -107,36 +228,34 @@ pre>code {
|
||||
content: attr(line);
|
||||
font-size: 13.5px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-line {
|
||||
.highlight-line {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-comp>code {
|
||||
.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%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import tailwindAnimate from "tailwindcss-animate";
|
||||
import typography from "@tailwindcss/typography";
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
@@ -105,7 +107,7 @@ const config = {
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||
plugins: [tailwindAnimate, typography],
|
||||
} satisfies Config;
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -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": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user