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;
|
const { meta } = docuConfig;
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params: {
|
params: Promise<{
|
||||||
slug: string[];
|
slug: string[];
|
||||||
};
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to generate metadata dynamically
|
// Function to generate metadata dynamically
|
||||||
export async function generateMetadata({ params: { slug = [] } }: PageProps) {
|
export async function generateMetadata(props: PageProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
|
||||||
|
const {
|
||||||
|
slug = []
|
||||||
|
} = params;
|
||||||
|
|
||||||
const pathName = slug.join("/");
|
const pathName = slug.join("/");
|
||||||
const res = await getDocsForSlug(pathName);
|
const res = await getDocsForSlug(pathName);
|
||||||
|
|
||||||
@@ -62,7 +68,13 @@ export async function generateMetadata({ params: { slug = [] } }: PageProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function DocsPage({ params: { slug = [] } }: PageProps) {
|
export default async function DocsPage(props: PageProps) {
|
||||||
|
const params = await props.params;
|
||||||
|
|
||||||
|
const {
|
||||||
|
slug = []
|
||||||
|
} = params;
|
||||||
|
|
||||||
const pathName = slug.join("/");
|
const pathName = slug.join("/");
|
||||||
const res = await getDocsForSlug(pathName);
|
const res = await getDocsForSlug(pathName);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { GeistMono } from "geist/font/mono";
|
|||||||
import { Footer } from "@/components/footer";
|
import { Footer } from "@/components/footer";
|
||||||
import docuConfig from "@/docu.json";
|
import docuConfig from "@/docu.json";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import "@docsearch/css";
|
||||||
|
import "@/styles/algolia.css";
|
||||||
|
import "@/styles/syntax.css";
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
|
||||||
const { meta } = docuConfig;
|
const { meta } = docuConfig;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-100 hover:duration-300 hover:dark:text-neutral-200">
|
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-100 hover:duration-300 hover:dark:text-neutral-200">
|
||||||
<span>🚀 New Version - Release v1.16.1</span>
|
<span>🚀 Release v2.0.0-beta.1</span>
|
||||||
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||||
</AnimatedShinyText>
|
</AnimatedShinyText>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setSearchedInput("");
|
setSearchedInput("");
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
@@ -71,9 +72,9 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|||||||
return advanceSearch(trimmedInput) as unknown as SearchResult[];
|
return advanceSearch(trimmedInput) as unknown as SearchResult[];
|
||||||
}, [searchedInput]);
|
}, [searchedInput]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
setSelectedIndex(0);
|
// setSelectedIndex(0);
|
||||||
}, [filteredResults]);
|
// }, [filteredResults]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNavigation = (event: KeyboardEvent) => {
|
const handleNavigation = (event: KeyboardEvent) => {
|
||||||
@@ -117,7 +118,10 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
value={searchedInput}
|
value={searchedInput}
|
||||||
onChange={(e) => setSearchedInput(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSearchedInput(e.target.value);
|
||||||
|
setSelectedIndex(0);
|
||||||
|
}}
|
||||||
placeholder="Type something to search..."
|
placeholder="Type something to search..."
|
||||||
autoFocus
|
autoFocus
|
||||||
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full"
|
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full"
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pathname.startsWith("/docs")) {
|
if (pathname.startsWith("/docs")) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setActiveRoute(getActiveContextRoute(pathname));
|
setActiveRoute(getActiveContextRoute(pathname));
|
||||||
} else {
|
} else {
|
||||||
setActiveRoute(undefined);
|
setActiveRoute(undefined);
|
||||||
@@ -61,7 +62,7 @@ export default function ContextPopover({ className }: ContextPopoverProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full max-w-[240px] flex items-center justify-between font-semibold text-foreground px-0 pt-8",
|
"w-full max-w-[240px] cursor-pointer flex items-center justify-between font-semibold text-foreground px-0 pt-8",
|
||||||
"hover:bg-transparent hover:text-foreground",
|
"hover:bg-transparent hover:text-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
import { type ThemeProviderProps } from "next-themes";
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function ToggleButton({
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
|
className="cursor-pointer hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
{collapsed ? (
|
{collapsed ? (
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ const Accordion: React.FC<AccordionProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
className="flex items-center space-x-2 w-full px-4 h-12 transition-colors bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70"
|
className="flex items-center gap-2 w-full px-4 h-12 transition-colors bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70"
|
||||||
>
|
>
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
"w-4 h-4 text-muted-foreground transition-transform duration-200 flex-shrink-0",
|
||||||
isOpen && "rotate-90"
|
isOpen && "rotate-90"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{Icon && <Icon className="text-foreground w-4 h-4"/> }
|
{Icon && <Icon className="text-foreground w-4 h-4 flex-shrink-0" />}
|
||||||
<h3 className="font-medium text-base text-foreground m-0">{title}</h3>
|
<h3 className="font-medium text-base text-foreground !m-0 leading-none">{title}</h3>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
|||||||
@@ -8,13 +8,21 @@ interface CardGroupProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className }) => {
|
const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className }) => {
|
||||||
const cardsArray = React.Children.toArray(children); // Pastikan children berupa array
|
const cardsArray = React.Children.toArray(children);
|
||||||
|
|
||||||
|
// Static grid column classes for Tailwind v4 compatibility
|
||||||
|
const gridColsClass = {
|
||||||
|
1: "grid-cols-1",
|
||||||
|
2: "grid-cols-1 sm:grid-cols-2",
|
||||||
|
3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
||||||
|
4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
|
||||||
|
}[cols] || "grid-cols-1 sm:grid-cols-2";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"grid gap-4 text-foreground",
|
"grid gap-4 text-foreground",
|
||||||
`grid-cols-1 sm:grid-cols-${cols}`,
|
gridColsClass,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export const Files = ({ children }: { children: ReactNode }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
rounded-xl border border-muted/50
|
rounded-xl border border-muted/20
|
||||||
bg-card/50 backdrop-blur-sm
|
bg-card/20 backdrop-blur-sm
|
||||||
shadow-sm overflow-hidden
|
shadow-sm overflow-hidden
|
||||||
transition-all duration-200
|
transition-all duration-200
|
||||||
hover:shadow-md hover:border-muted/60
|
hover:shadow-md hover:border-muted/60
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import NextImage from "next/image";
|
|||||||
type Height = ComponentProps<typeof NextImage>["height"];
|
type Height = ComponentProps<typeof NextImage>["height"];
|
||||||
type Width = ComponentProps<typeof NextImage>["width"];
|
type Width = ComponentProps<typeof NextImage>["width"];
|
||||||
|
|
||||||
|
type ImageProps = Omit<ComponentProps<"img">, "src"> & {
|
||||||
|
src?: ComponentProps<typeof NextImage>["src"];
|
||||||
|
};
|
||||||
|
|
||||||
export default function Image({
|
export default function Image({
|
||||||
src,
|
src,
|
||||||
alt = "alt",
|
alt = "alt",
|
||||||
width = 800,
|
width = 800,
|
||||||
height = 350,
|
height = 350,
|
||||||
...props
|
...props
|
||||||
}: ComponentProps<"img">) {
|
}: ImageProps) {
|
||||||
if (!src) return null;
|
if (!src) return null;
|
||||||
return (
|
return (
|
||||||
<NextImage
|
<NextImage
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type ComponentProps } from "react";
|
import { type ComponentProps, type JSX } from "react";
|
||||||
import Copy from "./CopyMdx";
|
import Copy from "./CopyMdx";
|
||||||
import {
|
import {
|
||||||
SiJavascript,
|
SiJavascript,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface MobTocProps {
|
|||||||
tocs: TocItem[];
|
tocs: TocItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const useClickOutside = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
|
const useClickOutside = (ref: React.RefObject<HTMLElement | null>, callback: () => void) => {
|
||||||
const handleClick = React.useCallback((event: MouseEvent) => {
|
const handleClick = React.useCallback((event: MouseEvent) => {
|
||||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||||
callback();
|
callback();
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function ScrollToTop({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initial check
|
// Initial check
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
checkScroll();
|
checkScroll();
|
||||||
|
|
||||||
// Set up scroll listener with debounce for better performance
|
// Set up scroll listener with debounce for better performance
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export default function SubLink({
|
|||||||
// Auto-expand if current path is a child of this item
|
// Auto-expand if current path is a child of this item
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (items && (path.startsWith(fullHref) && path !== fullHref)) {
|
if (items && (path.startsWith(fullHref) && path !== fullHref)) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
}
|
||||||
}, [path, fullHref, items]);
|
}, [path, fullHref, items]);
|
||||||
|
|||||||
@@ -1,65 +1,66 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Moon, Sun, Monitor } from "lucide-react";
|
import { Moon, Sun } from "lucide-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||||
const [selectedTheme, setSelectedTheme] = React.useState<string>("system");
|
const [mounted, setMounted] = React.useState(false);
|
||||||
|
|
||||||
// Pastikan toggle tetap di posisi yang benar setelah reload
|
// Untuk menghindari hydration mismatch
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (theme) {
|
setMounted(true);
|
||||||
setSelectedTheme(theme);
|
}, []);
|
||||||
} else {
|
|
||||||
setSelectedTheme("system"); // Default ke system jika undefined
|
// Jika belum mounted, jangan render apapun untuk menghindari mismatch
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 rounded-full border border-border bg-background/50 p-1">
|
||||||
|
<div className="rounded-full p-1 w-8 h-8" />
|
||||||
|
<div className="rounded-full p-1 w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [theme]);
|
|
||||||
|
// Tentukan theme yang aktif: gunakan resolvedTheme untuk menampilkan ikon yang sesuai
|
||||||
|
// jika theme === "system", resolvedTheme akan menjadi "light" atau "dark" sesuai device
|
||||||
|
const activeTheme = theme === "system" || !theme ? resolvedTheme : theme;
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
// Toggle antara light dan dark
|
||||||
|
// Jika sekarang light, ganti ke dark, dan sebaliknya
|
||||||
|
if (activeTheme === "light") {
|
||||||
|
setTheme("dark");
|
||||||
|
} else {
|
||||||
|
setTheme("light");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
value={selectedTheme}
|
value={activeTheme}
|
||||||
onValueChange={(value) => {
|
onValueChange={handleToggle}
|
||||||
if (value) {
|
|
||||||
setTheme(value);
|
|
||||||
setSelectedTheme(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-1 rounded-full border border-border bg-background/50 p-1 transition-all"
|
className="flex items-center gap-1 rounded-full border border-border bg-background/50 p-1 transition-all"
|
||||||
>
|
>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value="light"
|
value="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label="Light Mode"
|
aria-label="Light Mode"
|
||||||
className={`rounded-full p-1 transition-all ${
|
className={`rounded-full p-1 transition-all ${activeTheme === "light"
|
||||||
selectedTheme === "light"
|
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "bg-transparent hover:bg-muted/50"
|
: "bg-transparent hover:bg-muted/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Sun className="h-4 w-4" />
|
<Sun className="h-4 w-4" />
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
|
||||||
value="system"
|
|
||||||
size="sm"
|
|
||||||
aria-label="System Mode"
|
|
||||||
className={`rounded-full p-1 transition-all ${
|
|
||||||
selectedTheme === "system"
|
|
||||||
? "bg-primary text-primary-foreground"
|
|
||||||
: "bg-transparent hover:bg-muted/50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Monitor className="h-4 w-4" />
|
|
||||||
</ToggleGroupItem>
|
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value="dark"
|
value="dark"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label="Dark Mode"
|
aria-label="Dark Mode"
|
||||||
className={`rounded-full p-1 transition-all ${
|
className={`rounded-full p-1 transition-all ${activeTheme === "dark"
|
||||||
selectedTheme === "dark"
|
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "bg-transparent hover:bg-muted/50"
|
: "bg-transparent hover:bg-muted/50"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -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
|
// Add scroll event listener
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initial check
|
// Initial check
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
handleScroll();
|
handleScroll();
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
|||||||
83
package.json
83
package.json
@@ -1,62 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "docubook",
|
"name": "docubook",
|
||||||
"version": "1.16.1",
|
"version": "2.0.0-beta.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docsearch/css": "3",
|
"@docsearch/css": "^3.9.0",
|
||||||
"@docsearch/react": "^3.9.0",
|
"@docsearch/react": "^3.9.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-collapsible": "^1.1.0",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-popover": "^1.1.6",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toggle": "^1.1.2",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"algoliasearch": "^5.35.0",
|
"algoliasearch": "^5.46.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.0",
|
"cmdk": "^1.1.1",
|
||||||
"framer-motion": "^12.4.1",
|
"framer-motion": "^12.26.2",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.5.1",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"next": "^14.2.6",
|
"next": "16.1.3",
|
||||||
"next-mdx-remote": "^5.0.0",
|
"next-mdx-remote": "^5.0.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^18.3.1",
|
"react": "19.2.3",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "19.2.3",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-code-titles": "^1.2.0",
|
"rehype-code-titles": "^1.2.1",
|
||||||
"rehype-prism-plus": "^2.0.0",
|
"rehype-prism-plus": "^2.0.1",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^1.4.3",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"unist-util-visit": "^5.0.0"
|
"unist-util-visit": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.14",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^20",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/react": "^18",
|
"@types/node": "^20.19.30",
|
||||||
"@types/react-dom": "^18",
|
"@types/react": "19.2.8",
|
||||||
"autoprefixer": "^10.4.20",
|
"@types/react-dom": "19.2.3",
|
||||||
"eslint": "^8",
|
"autoprefixer": "^10.4.23",
|
||||||
"eslint-config-next": "^14.2.6",
|
"eslint": "^9.39.2",
|
||||||
"postcss": "^8",
|
"eslint-config-next": "16.1.3",
|
||||||
"tailwindcss": "^3.4.10",
|
"postcss": "^8.5.6",
|
||||||
"typescript": "^5"
|
"tailwindcss": "^4.1.18",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "19.2.8",
|
||||||
|
"@types/react-dom": "19.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,130 @@
|
|||||||
@import "@docsearch/css";
|
@import 'tailwindcss';
|
||||||
@import "./algolia.css";
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@import url("./syntax.css");
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@utility container {
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: 2rem;
|
||||||
|
|
||||||
|
@media (width >=--theme(--breakpoint-sm)) {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width >=1440px) {
|
||||||
|
max-width: 1440px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-border: hsl(var(--border));
|
||||||
|
--color-input: hsl(var(--input));
|
||||||
|
--color-ring: hsl(var(--ring));
|
||||||
|
--color-background: hsl(var(--background));
|
||||||
|
--color-foreground: hsl(var(--foreground));
|
||||||
|
|
||||||
|
--color-primary: hsl(var(--primary));
|
||||||
|
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||||
|
|
||||||
|
--color-secondary: hsl(var(--secondary));
|
||||||
|
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||||
|
|
||||||
|
--color-destructive: hsl(var(--destructive));
|
||||||
|
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||||
|
|
||||||
|
--color-muted: hsl(var(--muted));
|
||||||
|
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||||
|
|
||||||
|
--color-accent: hsl(var(--accent));
|
||||||
|
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||||
|
|
||||||
|
--color-popover: hsl(var(--popover));
|
||||||
|
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||||
|
|
||||||
|
--color-card: hsl(var(--card));
|
||||||
|
--color-card-foreground: hsl(var(--card-foreground));
|
||||||
|
|
||||||
|
--color-sidebar: hsl(var(--sidebar-background));
|
||||||
|
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
||||||
|
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
||||||
|
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
||||||
|
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
||||||
|
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
||||||
|
--color-sidebar-border: hsl(var(--sidebar-border));
|
||||||
|
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
||||||
|
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|
||||||
|
--font-code: var(--font-geist-mono);
|
||||||
|
--font-regular: var(--font-geist-sans);
|
||||||
|
|
||||||
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
--animate-shiny-text: shiny-text 8s infinite;
|
||||||
|
|
||||||
|
@keyframes accordion-down {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes accordion-up {
|
||||||
|
from {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shiny-text {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
90%,
|
||||||
|
100% {
|
||||||
|
background-position: calc(-100% - var(--shiny-width)) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
30%,
|
||||||
|
60% {
|
||||||
|
background-position: calc(100% + var(--shiny-width)) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||||
|
so we've added these compatibility styles to make sure everything still
|
||||||
|
looks the same as it did with Tailwind CSS v3.
|
||||||
|
|
||||||
|
If we ever want to remove these styles, we need to add an explicit border
|
||||||
|
color utility to any element that depends on these defaults.
|
||||||
|
*/
|
||||||
|
@layer base {
|
||||||
|
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: var(--color-gray-200, currentcolor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility animate-shine {
|
||||||
|
--animate-shine: shine var(--duration) infinite linear;
|
||||||
|
animation: var(--animate-shine);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Modern Blue Theme */
|
/* Modern Blue Theme */
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
@@ -15,7 +134,8 @@
|
|||||||
--card-foreground: 220 30% 15%;
|
--card-foreground: 220 30% 15%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 220 30% 15%;
|
--popover-foreground: 220 30% 15%;
|
||||||
--primary: 210 81% 56%; /* #2281E3 */
|
--primary: 210 81% 56%;
|
||||||
|
/* #2281E3 */
|
||||||
--primary-foreground: 0 0% 100%;
|
--primary-foreground: 0 0% 100%;
|
||||||
--secondary: 210 30% 90%;
|
--secondary: 210 30% 90%;
|
||||||
--secondary-foreground: 220 30% 15%;
|
--secondary-foreground: 220 30% 15%;
|
||||||
@@ -76,6 +196,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
.prose {
|
.prose {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -95,7 +216,7 @@ pre>code {
|
|||||||
|
|
||||||
.code-line {
|
.code-line {
|
||||||
padding: 0.75px 16px;
|
padding: 0.75px 16px;
|
||||||
@apply border-l-2 border-transparent
|
@apply border-l-2 border-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-number::before {
|
.line-number::before {
|
||||||
@@ -120,21 +241,19 @@ pre>code {
|
|||||||
.highlight-comp>code {
|
.highlight-comp>code {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.animate-shine {
|
|
||||||
--animate-shine: shine var(--duration) infinite linear;
|
|
||||||
animation: var(--animate-shine);
|
|
||||||
background-size: 200% 200%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shine {
|
@keyframes shine {
|
||||||
0% {
|
0% {
|
||||||
background-position: 0% 0%;
|
background-position: 0% 0%;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
background-position: 100% 100%;
|
background-position: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 0% 0%;
|
background-position: 0% 0%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
import tailwindAnimate from "tailwindcss-animate";
|
||||||
|
import typography from "@tailwindcss/typography";
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
darkMode: ["class"],
|
darkMode: "class",
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{ts,tsx}",
|
"./pages/**/*.{ts,tsx}",
|
||||||
"./components/**/*.{ts,tsx}",
|
"./components/**/*.{ts,tsx}",
|
||||||
@@ -105,7 +107,7 @@ const config = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
plugins: [tailwindAnimate, typography],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -10,7 +14,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -18,9 +22,20 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": [
|
||||||
}
|
"./*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"target": "ES2017"
|
||||||
"exclude": ["node_modules"]
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user