diff --git a/app/docs/[[...slug]]/page.tsx b/app/docs/[[...slug]]/page.tsx
index c4f6f05..47a49cc 100644
--- a/app/docs/[[...slug]]/page.tsx
+++ b/app/docs/[[...slug]]/page.tsx
@@ -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);
diff --git a/app/layout.tsx b/app/layout.tsx
index 2eb4c75..79d3fb4 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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;
diff --git a/app/page.tsx b/app/page.tsx
index 10c9130..7049c9f 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -25,21 +25,21 @@ export default function Home() {
)}
>
- 🚀 New Version - Release v1.16.1
+ 🚀 Release v2.0.0-beta.1
-
+
DocuBook Starter Templates
- Get started by editing app/page.tsx . Save and see your changes instantly.{' '}
-
+ Get started by editing app/page.tsx . Save and see your changes instantly.{' '}
+
Read Documentations
-
+
-
+
{
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) {
Search Documentation
Search through the documentation
-
+
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 (
-
- {
- itemRefs.current[index] = el as HTMLDivElement | null;
- }}
+
+ {
+ 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}
+ >
+ 1 && "border-l pl-4"
)}
- href={`/docs${item.href}`}
- tabIndex={-1}
- >
-
1 && "border-l pl-4"
- )}
- >
-
-
- {item.title}
-
- {isActive && (
-
- Return
-
-
- )}
+ >
+
+
+ {item.title}
-
-
+ {isActive && (
+
+ Return
+
+
+ )}
+
+
+
);
})}
@@ -177,14 +181,14 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
-
+
-
+
to navigate
-
+
to select
diff --git a/components/context-popover.tsx b/components/context-popover.tsx
index bbc454a..14ce4e7 100644
--- a/components/context-popover.tsx
+++ b/components/context-popover.tsx
@@ -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) {
{children};
diff --git a/components/leftbar.tsx b/components/leftbar.tsx
index 2264751..f6f9642 100644
--- a/components/leftbar.tsx
+++ b/components/leftbar.tsx
@@ -29,7 +29,7 @@ export function ToggleButton({
{collapsed ? (
diff --git a/components/markdown/AccordionMdx.tsx b/components/markdown/AccordionMdx.tsx
index 8e26e04..43c965d 100644
--- a/components/markdown/AccordionMdx.tsx
+++ b/components/markdown/AccordionMdx.tsx
@@ -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 = ({
// The main wrapper div for the accordion.
// All styling logic for the accordion container is handled here.
return (
- = ({
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"
>
- {Icon && }
- {title}
+ {Icon && }
+ {title}
{isOpen && (
diff --git a/components/markdown/CardGroupMdx.tsx b/components/markdown/CardGroupMdx.tsx
index a1423b8..3dda4fe 100644
--- a/components/markdown/CardGroupMdx.tsx
+++ b/components/markdown/CardGroupMdx.tsx
@@ -8,13 +8,21 @@ interface CardGroupProps {
}
const CardGroup: React.FC
= ({ 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 (
diff --git a/components/markdown/FileTreeMdx.tsx b/components/markdown/FileTreeMdx.tsx
index 6abdd3b..ee5ab79 100644
--- a/components/markdown/FileTreeMdx.tsx
+++ b/components/markdown/FileTreeMdx.tsx
@@ -100,8 +100,8 @@ export const Files = ({ children }: { children: ReactNode }) => {
return (
["height"];
type Width = ComponentProps
["width"];
+type ImageProps = Omit, "src"> & {
+ src?: ComponentProps["src"];
+};
+
export default function Image({
src,
alt = "alt",
width = 800,
height = 350,
...props
-}: ComponentProps<"img">) {
+}: ImageProps) {
if (!src) return null;
return (
, callback: () => void) => {
+const useClickOutside = (ref: React.RefObject, callback: () => void) => {
const handleClick = React.useCallback((event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback();
diff --git a/components/scroll-to-top.tsx b/components/scroll-to-top.tsx
index e072451..9dcd60d 100644
--- a/components/scroll-to-top.tsx
+++ b/components/scroll-to-top.tsx
@@ -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
diff --git a/components/sublink.tsx b/components/sublink.tsx
index eb5a425..ffb9195 100644
--- a/components/sublink.tsx
+++ b/components/sublink.tsx
@@ -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]);
diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx
index fa4c740..faee41a 100644
--- a/components/theme-toggle.tsx
+++ b/components/theme-toggle.tsx
@@ -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("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 (
+
+ );
+ }
+
+ // 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 (
{
- 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"
>
-
-
-
diff --git a/components/ui/icon-cloud.tsx b/components/ui/icon-cloud.tsx
deleted file mode 100644
index 1f6136e..0000000
--- a/components/ui/icon-cloud.tsx
+++ /dev/null
@@ -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(null);
- const [iconPositions, setIconPositions] = useState([]);
- 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();
- const rotationRef = useRef(rotation);
- const iconCanvasesRef = useRef([]);
- const imagesLoadedRef = useRef([]);
-
- // 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) => {
- 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) => {
- 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 (
-
- );
-}
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..1b53f6f
--- /dev/null
+++ b/eslint.config.mjs
@@ -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",
+ },
+}]);
\ No newline at end of file
diff --git a/hooks/useScrollPosition.ts b/hooks/useScrollPosition.ts
index 65905e8..3060aa4 100644
--- a/hooks/useScrollPosition.ts
+++ b/hooks/useScrollPosition.ts
@@ -2,27 +2,28 @@ import { useState, useCallback, useEffect } from 'react';
export function useScrollPosition(threshold = 0.5) {
const [isScrolled, setIsScrolled] = useState(false);
-
+
const handleScroll = useCallback(() => {
if (typeof window === 'undefined') return;
-
+
const scrollPosition = window.scrollY;
const viewportHeight = window.innerHeight;
const shouldBeSticky = scrollPosition > viewportHeight * threshold;
-
+
setIsScrolled(prev => shouldBeSticky !== prev ? shouldBeSticky : prev);
}, [threshold]);
// Add scroll event listener
useEffect(() => {
// Initial check
+ // eslint-disable-next-line react-hooks/set-state-in-effect
handleScroll();
-
+
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);
-
+
return isScrolled;
}
diff --git a/package.json b/package.json
index 17250be..0552629 100644
--- a/package.json
+++ b/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"
}
}
diff --git a/postcss.config.js b/postcss.config.js
index 12a703d..b4bee66 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,6 +1,6 @@
module.exports = {
plugins: {
- tailwindcss: {},
+ '@tailwindcss/postcss': {},
autoprefixer: {},
},
};
diff --git a/styles/globals.css b/styles/globals.css
index 84669c5..6c012de 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -1,69 +1,189 @@
-@import "@docsearch/css";
-@import "./algolia.css";
+@import 'tailwindcss';
+@plugin '@tailwindcss/typography';
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-@import url("./syntax.css");
+@custom-variant dark (&:is(.dark *));
+
+@utility container {
+ margin-inline: auto;
+ padding-inline: 2rem;
+
+ @media (width >=--theme(--breakpoint-sm)) {
+ max-width: none;
+ }
+
+ @media (width >=1440px) {
+ max-width: 1440px;
+ }
+}
+
+@theme {
+ --color-border: hsl(var(--border));
+ --color-input: hsl(var(--input));
+ --color-ring: hsl(var(--ring));
+ --color-background: hsl(var(--background));
+ --color-foreground: hsl(var(--foreground));
+
+ --color-primary: hsl(var(--primary));
+ --color-primary-foreground: hsl(var(--primary-foreground));
+
+ --color-secondary: hsl(var(--secondary));
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
+
+ --color-destructive: hsl(var(--destructive));
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
+
+ --color-muted: hsl(var(--muted));
+ --color-muted-foreground: hsl(var(--muted-foreground));
+
+ --color-accent: hsl(var(--accent));
+ --color-accent-foreground: hsl(var(--accent-foreground));
+
+ --color-popover: hsl(var(--popover));
+ --color-popover-foreground: hsl(var(--popover-foreground));
+
+ --color-card: hsl(var(--card));
+ --color-card-foreground: hsl(var(--card-foreground));
+
+ --color-sidebar: hsl(var(--sidebar-background));
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
+ --color-sidebar-border: hsl(var(--sidebar-border));
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
+
+ --radius-lg: var(--radius);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-sm: calc(var(--radius) - 4px);
+
+ --font-code: var(--font-geist-mono);
+ --font-regular: var(--font-geist-sans);
+
+ --animate-accordion-down: accordion-down 0.2s ease-out;
+ --animate-accordion-up: accordion-up 0.2s ease-out;
+ --animate-shiny-text: shiny-text 8s infinite;
+
+ @keyframes accordion-down {
+ from {
+ height: 0;
+ }
+
+ to {
+ height: var(--radix-accordion-content-height);
+ }
+ }
+
+ @keyframes accordion-up {
+ from {
+ height: var(--radix-accordion-content-height);
+ }
+
+ to {
+ height: 0;
+ }
+ }
+
+ @keyframes shiny-text {
+
+ 0%,
+ 90%,
+ 100% {
+ background-position: calc(-100% - var(--shiny-width)) 0;
+ }
+
+ 30%,
+ 60% {
+ background-position: calc(100% + var(--shiny-width)) 0;
+ }
+ }
+}
+
+/*
+ The default border color has changed to `currentcolor` in Tailwind CSS v4,
+ so we've added these compatibility styles to make sure everything still
+ looks the same as it did with Tailwind CSS v3.
+
+ If we ever want to remove these styles, we need to add an explicit border
+ color utility to any element that depends on these defaults.
+*/
+@layer base {
+
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+}
+
+@utility animate-shine {
+ --animate-shine: shine var(--duration) infinite linear;
+ animation: var(--animate-shine);
+ background-size: 200% 200%;
+}
+
/* Modern Blue Theme */
@layer base {
- :root {
- --background: 210 40% 98%;
- --foreground: 220 30% 15%;
- --card: 0 0% 100%;
- --card-foreground: 220 30% 15%;
- --popover: 0 0% 100%;
- --popover-foreground: 220 30% 15%;
- --primary: 210 81% 56%; /* #2281E3 */
- --primary-foreground: 0 0% 100%;
- --secondary: 210 30% 90%;
- --secondary-foreground: 220 30% 15%;
- --muted: 210 20% 92%;
- --muted-foreground: 220 15% 50%;
- --accent: 200 100% 40%;
- --accent-foreground: 0 0% 100%;
- --destructive: 0 85% 60%;
- --destructive-foreground: 0 0% 100%;
- --border: 210 20% 85%;
- --input: 210 20% 85%;
- --ring: 210 81% 56%;
- --radius: 0.5rem;
- --chart-1: 210 81% 56%;
- --chart-2: 200 100% 40%;
- --chart-3: 220 76% 60%;
- --chart-4: 190 90% 50%;
- --chart-5: 230 86% 45%;
- --line-number-color: rgba(0, 0, 0, 0.05);
- }
+ :root {
+ --background: 210 40% 98%;
+ --foreground: 220 30% 15%;
+ --card: 0 0% 100%;
+ --card-foreground: 220 30% 15%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 220 30% 15%;
+ --primary: 210 81% 56%;
+ /* #2281E3 */
+ --primary-foreground: 0 0% 100%;
+ --secondary: 210 30% 90%;
+ --secondary-foreground: 220 30% 15%;
+ --muted: 210 20% 92%;
+ --muted-foreground: 220 15% 50%;
+ --accent: 200 100% 40%;
+ --accent-foreground: 0 0% 100%;
+ --destructive: 0 85% 60%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 210 20% 85%;
+ --input: 210 20% 85%;
+ --ring: 210 81% 56%;
+ --radius: 0.5rem;
+ --chart-1: 210 81% 56%;
+ --chart-2: 200 100% 40%;
+ --chart-3: 220 76% 60%;
+ --chart-4: 190 90% 50%;
+ --chart-5: 230 86% 45%;
+ --line-number-color: rgba(0, 0, 0, 0.05);
+ }
- .dark {
- --background: 220 25% 10%;
- --foreground: 210 30% 96%;
- --card: 220 25% 15%;
- --card-foreground: 210 30% 96%;
- --popover: 220 25% 15%;
- --popover-foreground: 210 30% 96%;
- --primary: 210 100% 65%;
- --primary-foreground: 220 25% 10%;
- --secondary: 215 25% 20%;
- --secondary-foreground: 210 30% 96%;
- --muted: 215 20% 25%;
- --muted-foreground: 215 20% 65%;
- --accent: 200 100% 60%;
- --accent-foreground: 0 0% 100%;
- --destructive: 0 85% 70%;
- --destructive-foreground: 0 0% 100%;
- --border: 215 20% 25%;
- --input: 215 20% 25%;
- --ring: 210 100% 65%;
- --chart-1: 210 100% 65%;
- --chart-2: 200 100% 60%;
- --chart-3: 220 90% 70%;
- --chart-4: 190 100% 65%;
- --chart-5: 230 90% 60%;
- --line-number-color: rgba(255, 255, 255, 0.1);
- }
+ .dark {
+ --background: 220 25% 10%;
+ --foreground: 210 30% 96%;
+ --card: 220 25% 15%;
+ --card-foreground: 210 30% 96%;
+ --popover: 220 25% 15%;
+ --popover-foreground: 210 30% 96%;
+ --primary: 210 100% 65%;
+ --primary-foreground: 220 25% 10%;
+ --secondary: 215 25% 20%;
+ --secondary-foreground: 210 30% 96%;
+ --muted: 215 20% 25%;
+ --muted-foreground: 215 20% 65%;
+ --accent: 200 100% 60%;
+ --accent-foreground: 0 0% 100%;
+ --destructive: 0 85% 70%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 215 20% 25%;
+ --input: 215 20% 25%;
+ --ring: 210 100% 65%;
+ --chart-1: 210 100% 65%;
+ --chart-2: 200 100% 60%;
+ --chart-3: 220 90% 70%;
+ --chart-4: 190 100% 65%;
+ --chart-5: 230 90% 60%;
+ --line-number-color: rgba(255, 255, 255, 0.1);
+ }
}
@layer base {
@@ -76,67 +196,66 @@
}
}
-.prose {
- margin: 0 !important;
-}
+@layer utilities {
+ .prose {
+ margin: 0 !important;
+ }
-pre {
- padding: 2px 0 !important;
- width: inherit !important;
- overflow-x: auto;
-}
+ pre {
+ padding: 2px 0 !important;
+ width: inherit !important;
+ overflow-x: auto;
+ }
-pre>code {
- display: grid;
- max-width: inherit !important;
- padding: 14px 0 !important;
- border: 0 !important;
-}
+ pre>code {
+ display: grid;
+ max-width: inherit !important;
+ padding: 14px 0 !important;
+ border: 0 !important;
+ }
-.code-line {
- padding: 0.75px 16px;
- @apply border-l-2 border-transparent
-}
+ .code-line {
+ padding: 0.75px 16px;
+ @apply border-l-2 border-transparent;
+ }
-.line-number::before {
- display: inline-block;
- width: 1rem;
- margin-right: 22px;
- margin-left: -2px;
- color: rgb(110, 110, 110);
- content: attr(line);
- font-size: 13.5px;
- text-align: right;
-}
+ .line-number::before {
+ display: inline-block;
+ width: 1rem;
+ margin-right: 22px;
+ margin-left: -2px;
+ color: rgb(110, 110, 110);
+ content: attr(line);
+ font-size: 13.5px;
+ text-align: right;
+ }
-.highlight-line {
- @apply bg-primary/5 border-l-2 border-primary/30;
-}
+ .highlight-line {
+ @apply bg-primary/5 border-l-2 border-primary/30;
+ }
-.rehype-code-title {
- @apply px-2 -mb-8 w-full text-sm pb-5 font-medium mt-5 font-code;
-}
+ .rehype-code-title {
+ @apply px-2 -mb-8 w-full text-sm pb-5 font-medium mt-5 font-code;
+ }
-.highlight-comp>code {
- background-color: transparent !important;
+ .highlight-comp>code {
+ background-color: transparent !important;
+ }
}
@layer utilities {
- .animate-shine {
- --animate-shine: shine var(--duration) infinite linear;
- animation: var(--animate-shine);
- background-size: 200% 200%;
+
+ @keyframes shine {
+ 0% {
+ background-position: 0% 0%;
}
- @keyframes shine {
- 0% {
- background-position: 0% 0%;
- }
- 50% {
- background-position: 100% 100%;
- }
- 100% {
- background-position: 0% 0%;
- }
+ 50% {
+ background-position: 100% 100%;
+ }
+
+ 100% {
+ background-position: 0% 0%;
}
}
+}
\ No newline at end of file
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 56783f3..8e6fa3b 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,111 +1,113 @@
import type { Config } from "tailwindcss";
+import tailwindAnimate from "tailwindcss-animate";
+import typography from "@tailwindcss/typography";
const config = {
- darkMode: ["class"],
- content: [
- "./pages/**/*.{ts,tsx}",
- "./components/**/*.{ts,tsx}",
- "./app/**/*.{ts,tsx}",
- "./src/**/*.{ts,tsx}",
- ],
- prefix: "",
- theme: {
- container: {
- center: true,
- padding: '2rem',
- screens: {
- '2xl': '1440px'
- }
- },
- extend: {
- colors: {
- border: 'hsl(var(--border))',
- input: 'hsl(var(--input))',
- ring: 'hsl(var(--ring))',
- background: 'hsl(var(--background))',
- foreground: 'hsl(var(--foreground))',
- primary: {
- DEFAULT: 'hsl(var(--primary))',
- foreground: 'hsl(var(--primary-foreground))'
- },
- secondary: {
- DEFAULT: 'hsl(var(--secondary))',
- foreground: 'hsl(var(--secondary-foreground))'
- },
- destructive: {
- DEFAULT: 'hsl(var(--destructive))',
- foreground: 'hsl(var(--destructive-foreground))'
- },
- muted: {
- DEFAULT: 'hsl(var(--muted))',
- foreground: 'hsl(var(--muted-foreground))'
- },
- accent: {
- DEFAULT: 'hsl(var(--accent))',
- foreground: 'hsl(var(--accent-foreground))'
- },
- popover: {
- DEFAULT: 'hsl(var(--popover))',
- foreground: 'hsl(var(--popover-foreground))'
- },
- card: {
- DEFAULT: 'hsl(var(--card))',
- foreground: 'hsl(var(--card-foreground))'
- },
- sidebar: {
- DEFAULT: 'hsl(var(--sidebar-background))',
- foreground: 'hsl(var(--sidebar-foreground))',
- primary: 'hsl(var(--sidebar-primary))',
- 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
- accent: 'hsl(var(--sidebar-accent))',
- 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
- border: 'hsl(var(--sidebar-border))',
- ring: 'hsl(var(--sidebar-ring))'
- }
- },
- borderRadius: {
- lg: 'var(--radius)',
- md: 'calc(var(--radius) - 2px)',
- sm: 'calc(var(--radius) - 4px)'
- },
- fontFamily: {
- code: ["var(--font-geist-mono)"],
- regular: ["var(--font-geist-sans)"]
- },
- keyframes: {
- 'accordion-down': {
- from: {
- height: '0'
- },
- to: {
- height: 'var(--radix-accordion-content-height)'
- }
- },
- 'accordion-up': {
- from: {
- height: 'var(--radix-accordion-content-height)'
- },
- to: {
- height: '0'
- }
- },
- 'shiny-text': {
- '0%, 90%, 100%': {
- 'background-position': 'calc(-100% - var(--shiny-width)) 0'
- },
- '30%, 60%': {
- 'background-position': 'calc(100% + var(--shiny-width)) 0'
- }
- }
- },
- animation: {
- 'accordion-down': 'accordion-down 0.2s ease-out',
- 'accordion-up': 'accordion-up 0.2s ease-out',
- 'shiny-text': 'shiny-text 8s infinite'
- }
- }
- },
- plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
+ darkMode: "class",
+ content: [
+ "./pages/**/*.{ts,tsx}",
+ "./components/**/*.{ts,tsx}",
+ "./app/**/*.{ts,tsx}",
+ "./src/**/*.{ts,tsx}",
+ ],
+ prefix: "",
+ theme: {
+ container: {
+ center: true,
+ padding: '2rem',
+ screens: {
+ '2xl': '1440px'
+ }
+ },
+ extend: {
+ colors: {
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))'
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))'
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))'
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))'
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))'
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))'
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))'
+ },
+ sidebar: {
+ DEFAULT: 'hsl(var(--sidebar-background))',
+ foreground: 'hsl(var(--sidebar-foreground))',
+ primary: 'hsl(var(--sidebar-primary))',
+ 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
+ accent: 'hsl(var(--sidebar-accent))',
+ 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
+ border: 'hsl(var(--sidebar-border))',
+ ring: 'hsl(var(--sidebar-ring))'
+ }
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)'
+ },
+ fontFamily: {
+ code: ["var(--font-geist-mono)"],
+ regular: ["var(--font-geist-sans)"]
+ },
+ keyframes: {
+ 'accordion-down': {
+ from: {
+ height: '0'
+ },
+ to: {
+ height: 'var(--radix-accordion-content-height)'
+ }
+ },
+ 'accordion-up': {
+ from: {
+ height: 'var(--radix-accordion-content-height)'
+ },
+ to: {
+ height: '0'
+ }
+ },
+ 'shiny-text': {
+ '0%, 90%, 100%': {
+ 'background-position': 'calc(-100% - var(--shiny-width)) 0'
+ },
+ '30%, 60%': {
+ 'background-position': 'calc(100% + var(--shiny-width)) 0'
+ }
+ }
+ },
+ animation: {
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ 'shiny-text': 'shiny-text 8s infinite'
+ }
+ }
+ },
+ plugins: [tailwindAnimate, typography],
} satisfies Config;
export default config;
diff --git a/tsconfig.json b/tsconfig.json
index e7ff90f..9e9bbf7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,10 @@
{
"compilerOptions": {
- "lib": ["dom", "dom.iterable", "esnext"],
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -10,7 +14,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "preserve",
+ "jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -18,9 +22,20 @@
}
],
"paths": {
- "@/*": ["./*"]
- }
+ "@/*": [
+ "./*"
+ ]
+ },
+ "target": "ES2017"
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
}