diff --git a/.gitignore b/.gitignore index 73123d0..992a671 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,9 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc +.next/ .DS_Store *.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel +.env* .vercel - -# typescript -*.tsbuildinfo next-env.d.ts - -# bun bun.lock +node_modules + diff --git a/app/docs/[[...slug]]/page.tsx b/app/docs/[[...slug]]/page.tsx index e765173..ca2b21b 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,13 +68,19 @@ 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); if (!res) notFound(); - const { title, description, image, date } = res.frontmatter; + const { title, description, image: _image, date } = res.frontmatter; // File path for edit link const filePath = `contents/docs/${slug.join("/") || ""}/index.mdx`; @@ -85,17 +97,16 @@ export default async function DocsPage({ params: { slug = [] } }: PageProps) {

{description}

{res.content}
+ className={`my-8 flex items-center border-b-2 border-dashed border-x-muted-foreground ${docuConfig.repository?.editLink ? "justify-between" : "justify-end" + }`} + > {docuConfig.repository?.editLink && } {date && ( -

+

Published on {formatDate2(date)} -

+

)} -
+ 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/components/DocSearch.tsx b/components/DocSearch.tsx new file mode 100644 index 0000000..e63b375 --- /dev/null +++ b/components/DocSearch.tsx @@ -0,0 +1,32 @@ +"use client"; + +import React from "react"; +import { DocSearch } from "@docsearch/react"; + +export default function DocSearchComponent() { + const appId = process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_APP_ID; + const apiKey = process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_API_KEY; + const indexName = process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_INDEX_NAME; + + if (!appId || !apiKey || !indexName) { + console.error( + "DocSearch credentials are not set in the environment variables." + ); + return ( + + ); + } + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/components/SearchModal.tsx b/components/SearchModal.tsx new file mode 100644 index 0000000..399b53f --- /dev/null +++ b/components/SearchModal.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect, useMemo, useState, useRef } from "react"; +import { ArrowUpIcon, ArrowDownIcon, CornerDownLeftIcon, FileTextIcon } from "lucide-react"; +import Anchor from "./anchor"; +import { advanceSearch, cn } from "@/lib/utils"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { page_routes } from "@/lib/routes-config"; +import { + DialogContent, + DialogHeader, + DialogFooter, + DialogClose, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; + +type ContextInfo = { + icon: string; + description: string; + title?: string; +}; + +type SearchResult = { + title: string; + href: string; + noLink?: boolean; + items?: undefined; + score?: number; + context?: ContextInfo; +}; + +const paddingMap = { + 1: "pl-2", + 2: "pl-4", + 3: "pl-10", +} as const; + +interface SearchModalProps { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +} + +export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) { + const router = useRouter(); + const [searchedInput, setSearchedInput] = useState(""); + const [selectedIndex, setSelectedIndex] = useState(0); + const itemRefs = useRef<(HTMLDivElement | null)[]>([]); + + useEffect(() => { + if (!isOpen) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setSearchedInput(""); + } + }, [isOpen]); + + const filteredResults = useMemo(() => { + const trimmedInput = searchedInput.trim(); + + if (trimmedInput.length < 3) { + return page_routes + .filter((route) => !route.href.endsWith('/')) + .slice(0, 6) + .map((route: { title: string; href: string; noLink?: boolean; context?: ContextInfo }) => ({ + title: route.title, + href: route.href, + noLink: route.noLink, + context: route.context, + })); + } + return advanceSearch(trimmedInput) as unknown as SearchResult[]; + }, [searchedInput]); + + // useEffect(() => { + // setSelectedIndex(0); + // }, [filteredResults]); + + useEffect(() => { + const handleNavigation = (event: KeyboardEvent) => { + if (!isOpen || filteredResults.length === 0) return; + + if (event.key === "ArrowDown") { + event.preventDefault(); + setSelectedIndex((prev) => (prev + 1) % filteredResults.length); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length); + } else if (event.key === "Enter") { + event.preventDefault(); + const selectedItem = filteredResults[selectedIndex]; + if (selectedItem) { + router.push(`/docs${selectedItem.href}`); + setIsOpen(false); + } + } + }; + + window.addEventListener("keydown", handleNavigation); + return () => window.removeEventListener("keydown", handleNavigation); + }, [isOpen, filteredResults, selectedIndex, router, setIsOpen]); + + useEffect(() => { + if (itemRefs.current[selectedIndex]) { + itemRefs.current[selectedIndex]?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + } + }, [selectedIndex]); + + return ( + + + Search Documentation + Search through the documentation + + + { + 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" + aria-label="Search documentation" + /> + + {filteredResults.length == 0 && searchedInput && ( +

+ No results found for{" "} + {`"${searchedInput}"`} +

+ )} + +
+ {filteredResults.map((item, index) => { + const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap; + const paddingClass = paddingMap[level] || 'pl-2'; + const isActive = index === selectedIndex; + + return ( + + { + 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" + )} + > +
+ + {item.title} +
+ {isActive && ( +
+ Return + +
+ )} +
+
+
+ ); + })} +
+
+ +
+ + + + + + +

to navigate

+ + + +

to select

+ + esc + +

to close

+
+
+
+ ); +} \ No newline at end of file diff --git a/components/SearchTrigger.tsx b/components/SearchTrigger.tsx new file mode 100644 index 0000000..055d407 --- /dev/null +++ b/components/SearchTrigger.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { CommandIcon, SearchIcon } from "lucide-react"; +import { DialogTrigger } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; + +export function SearchTrigger() { + return ( + +
+
+
+ +
+
+ + +
+ + K +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/Sponsor.tsx b/components/Sponsor.tsx index 783192d..def5ce0 100644 --- a/components/Sponsor.tsx +++ b/components/Sponsor.tsx @@ -10,16 +10,76 @@ interface SponsorItem { description?: string; } +interface NavbarConfig { + title?: string; + logo?: { + light?: string; + dark?: string; + }; + links?: Array<{ + title: string; + href: string; + external?: boolean; + }>; +} + +interface FooterConfig { + text?: string; + links?: Array<{ + title: string; + href: string; + external?: boolean; + }>; +} + +interface MetaConfig { + title?: string; + description?: string; + favicon?: string; + socialBanner?: string; +} + +interface RepositoryConfig { + url: string; + editUrl?: string; + branch?: string; + directory?: string; +} + +interface RouteItem { + title: string; + href: string; + noLink?: boolean; + context?: { + icon: string; + description: string; + title: string; + }; + items?: RouteItem[]; +} + +interface RouteConfig { + title: string; + href: string; + noLink?: boolean; + context?: { + icon: string; + description: string; + title: string; + }; + items?: RouteItem[]; +} + interface DocuConfig { sponsor?: { title?: string; item?: SponsorItem; }; - navbar: any; // Anda bisa mendefinisikan tipe yang lebih spesifik jika diperlukan - footer: any; - meta: any; - repository: any; - routes: any[]; + navbar: NavbarConfig; + footer: FooterConfig; + meta: MetaConfig; + repository: RepositoryConfig; + routes: RouteConfig[]; } // Type assertion for docu.json diff --git a/components/context-popover.tsx b/components/context-popover.tsx index bbc454a..3b862aa 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) { {isOpen && ( -
+
{children}
)} @@ -44,4 +59,4 @@ const Accordion = ({ ); }; -export default Accordion; +export default Accordion; \ No newline at end of file diff --git a/components/markdown/ButtonMdx.tsx b/components/markdown/ButtonMdx.tsx index e0a5a72..eee7404 100644 --- a/components/markdown/ButtonMdx.tsx +++ b/components/markdown/ButtonMdx.tsx @@ -1,8 +1,6 @@ import React from "react"; import * as Icons from "lucide-react"; import Link from "next/link"; - -type IconName = keyof typeof Icons; type ButtonProps = { icon?: keyof typeof Icons; text?: string; 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/CopyMdx.tsx b/components/markdown/CopyMdx.tsx index d14499b..dec9e91 100644 --- a/components/markdown/CopyMdx.tsx +++ b/components/markdown/CopyMdx.tsx @@ -19,7 +19,7 @@ export default function Copy({ content }: { content: string }) { return ( + + + {isOpen && ( + + setIsOpen(false)} + > + {/* Close Button */} + + + {/* Image Container */} + e.stopPropagation()} + > +
setIsOpen(false)}> + +
+
+ + {/* Caption */} + {alt && alt !== "alt" && ( + + {alt} + + )} + +
+
+ )} +
+ + ); } + +const Portal = ({ children }: { children: React.ReactNode }) => { + if (typeof window === "undefined") return null; + return createPortal(children, document.body); +}; diff --git a/components/markdown/NoteMdx.tsx b/components/markdown/NoteMdx.tsx index 2627737..9d2313c 100644 --- a/components/markdown/NoteMdx.tsx +++ b/components/markdown/NoteMdx.tsx @@ -1,52 +1,69 @@ +"use client"; + import { cn } from "@/lib/utils"; -import clsx from "clsx"; -import { PropsWithChildren } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; import { Info, AlertTriangle, ShieldAlert, - CheckCircle, + CheckCircle2, } from "lucide-react"; +import React from "react"; -type NoteProps = PropsWithChildren & { - title?: string; - type?: "note" | "danger" | "warning" | "success"; -}; +const noteVariants = cva( + "relative w-full rounded-lg border border-l-4 p-4 mb-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + note: "bg-muted/30 border-border border-l-primary/50 text-foreground [&>svg]:text-primary", + danger: "border-destructive/20 border-l-destructive/60 bg-destructive/5 text-destructive [&>svg]:text-destructive dark:border-destructive/30", + warning: "border-orange-500/20 border-l-orange-500/60 bg-orange-500/5 text-orange-600 dark:text-orange-400 [&>svg]:text-orange-600 dark:[&>svg]:text-orange-400", + success: "border-emerald-500/20 border-l-emerald-500/60 bg-emerald-500/5 text-emerald-600 dark:text-emerald-400 [&>svg]:text-emerald-600 dark:[&>svg]:text-emerald-400", + }, + }, + defaultVariants: { + variant: "note", + }, + } +); const iconMap = { - note: , - danger: , - warning: , - success: , + note: Info, + danger: ShieldAlert, + warning: AlertTriangle, + success: CheckCircle2, }; +interface NoteProps + extends React.HTMLAttributes, + VariantProps { + title?: string; + type?: "note" | "danger" | "warning" | "success"; +} + export default function Note({ - children, + className, title = "Note", type = "note", + children, + ...props }: NoteProps) { - const noteClassNames = clsx({ - "dark:bg-stone-950/25 bg-stone-50": type === "note", - "dark:bg-red-950 bg-red-100 border-red-200 dark:border-red-900": - type === "danger", - "bg-orange-50 border-orange-200 dark:border-orange-900 dark:bg-orange-900/50": - type === "warning", - "dark:bg-green-950 bg-green-100 border-green-200 dark:border-green-900": - type === "success", - }); + const Icon = iconMap[type] || Info; return (
-
- {iconMap[type]} - {title}: + +
+
+ {title} +
+
+ {children} +
- {children}
); } diff --git a/components/markdown/PreMdx.tsx b/components/markdown/PreMdx.tsx index 172211c..f359093 100644 --- a/components/markdown/PreMdx.tsx +++ b/components/markdown/PreMdx.tsx @@ -1,19 +1,109 @@ -import { ComponentProps } from "react"; +import { type ComponentProps, type JSX } from "react"; import Copy from "./CopyMdx"; +import { + SiJavascript, + SiTypescript, + SiReact, + SiPython, + SiGo, + SiPhp, + SiRuby, + SiSwift, + SiKotlin, + SiHtml5, + SiCss3, + SiSass, + SiPostgresql, + SiGraphql, + SiYaml, + SiToml, + SiDocker, + SiNginx, + SiGit, + SiGnubash, + SiMarkdown, +} from "react-icons/si"; +import { FaJava, FaCode } from "react-icons/fa"; +import { TbJson } from "react-icons/tb"; + +type PreProps = ComponentProps<"pre"> & { + raw?: string; + "data-title"?: string; +}; + +// Component to display an icon based on the programming language +const LanguageIcon = ({ lang }: { lang: string }) => { + const iconProps = { className: "w-4 h-4" }; + const languageToIconMap: Record = { + gitignore: , + docker: , + dockerfile: , + nginx: , + sql: , + graphql: , + yaml: , + yml: , + toml: , + json: , + md: , + markdown: , + bash: , + sh: , + shell: , + swift: , + kotlin: , + kt: , + kts: , + rb: , + ruby: , + php: , + go: , + py: , + python: , + java: , + tsx: , + typescript: , + ts: , + jsx: , + js: , + javascript: , + html: , + css: , + scss: , + sass: , + }; + return languageToIconMap[lang] || ; +}; + +// Function to extract the language from className +function getLanguage(className: string = ""): string { + const match = className.match(/language-(\w+)/); + return match ? match[1] : "default"; +} + +export default function Pre({ children, raw, ...rest }: PreProps) { + const { "data-title": title, className, ...restProps } = rest; + const language = getLanguage(className); + const hasTitle = !!title; -export default function Pre({ - children, - raw, - ...rest -}: ComponentProps<"pre"> & { raw?: string }) { return ( -
-
- +
+
+ {raw && }
-
-
{children}
+ {hasTitle && ( +
+
+ + {title} +
+
+ )} +
+
+          {children}
+        
); -} +} \ No newline at end of file diff --git a/components/markdown/mdx-provider.tsx b/components/markdown/mdx-provider.tsx index 8265b8f..b2e6e0d 100644 --- a/components/markdown/mdx-provider.tsx +++ b/components/markdown/mdx-provider.tsx @@ -1,6 +1,6 @@ 'use client'; -import { MDXRemote, MDXRemoteProps } from 'next-mdx-remote/rsc'; +import { MDXRemote } from 'next-mdx-remote/rsc'; import { Kbd } from './KeyboardMdx'; // Define components mapping diff --git a/components/mob-toc.tsx b/components/mob-toc.tsx index 6a23a0a..6d37696 100644 --- a/components/mob-toc.tsx +++ b/components/mob-toc.tsx @@ -7,14 +7,14 @@ import { useRef, useMemo } from "react"; import { usePathname } from "next/navigation"; import { Button } from "./ui/button"; import { motion, AnimatePresence } from "framer-motion"; -import { useScrollPosition, useActiveSection } from "@/hooks"; +import { useActiveSection } from "@/hooks"; import { TocItem } from "@/lib/toc"; interface MobTocProps { tocs: TocItem[]; } -const useClickOutside = (ref: React.RefObject, 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/search.tsx b/components/search.tsx index 7509d88..8a6a024 100644 --- a/components/search.tsx +++ b/components/search.tsx @@ -1,246 +1,55 @@ "use client"; -import { useRouter } from "next/navigation"; -import { useEffect, useMemo, useState, useRef } from "react"; -import { ArrowUpIcon, ArrowDownIcon, CommandIcon, FileTextIcon, SearchIcon, CornerDownLeftIcon } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogFooter, - DialogTrigger, - DialogClose, - DialogTitle, - DialogDescription, -} from "@/components/ui/dialog"; -import Anchor from "./anchor"; -import { advanceSearch, cn } from "@/lib/utils"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { page_routes } from "@/lib/routes-config"; +import { useState, useEffect } from "react"; +import { Dialog } from "@/components/ui/dialog"; +import { SearchTrigger } from "@/components/SearchTrigger"; +import { SearchModal } from "@/components/SearchModal"; +import DocSearchComponent from "@/components/DocSearch"; +import { DialogTrigger } from "@radix-ui/react-dialog"; -// Define the ContextInfo type to match the one in routes-config -type ContextInfo = { - icon: string; - description: string; - title?: string; -}; +interface SearchProps { + /** + * Specify which search engine to use. + * @default 'default' + */ + type?: "default" | "algolia"; +} -type SearchResult = { - title: string; - href: string; - noLink?: boolean; - items?: undefined; - score?: number; - context?: ContextInfo; -}; - -export default function Search() { - const router = useRouter(); - const [searchedInput, setSearchedInput] = useState(""); +export default function Search({ type = "default" }: SearchProps) { const [isOpen, setIsOpen] = useState(false); - const [selectedIndex, setSelectedIndex] = useState(0); - const itemRefs = useRef<(HTMLDivElement | null)[]>([]); + // The useEffect below is ONLY for the 'default' type, which is correct. + // DocSearch handles its own keyboard shortcut. useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ((event.ctrlKey || event.metaKey) && event.key === "k") { - event.preventDefault(); - setIsOpen(true); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, []); - - const filteredResults = useMemo(() => { - const trimmedInput = searchedInput.trim(); - - // If search input is empty or less than 3 characters, show initial suggestions - if (trimmedInput.length < 3) { - return page_routes - .filter((route: { href: string }) => !route.href.endsWith('/')) // Filter out directory routes - .slice(0, 6) // Limit to 6 posts - .map((route: { title: string; href: string; noLink?: boolean; context?: ContextInfo }) => ({ - title: route.title, - href: route.href, - noLink: route.noLink, - context: route.context - })); - } - - // For search with 3 or more characters, use the advance search - return advanceSearch(trimmedInput) as unknown as SearchResult[]; - }, [searchedInput]); - - useEffect(() => { - setSelectedIndex(0); - }, [filteredResults]); - - useEffect(() => { - const handleNavigation = (event: KeyboardEvent) => { - if (!isOpen || filteredResults.length === 0) return; - - if (event.key === "ArrowDown") { - event.preventDefault(); - setSelectedIndex((prev) => (prev + 1) % filteredResults.length); - } - - if (event.key === "ArrowUp") { - event.preventDefault(); - setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length); - } - - if (event.key === "Enter") { - event.preventDefault(); - const selectedItem = filteredResults[selectedIndex]; - if (selectedItem) { - router.push(`/docs${selectedItem.href}`); - setIsOpen(false); + if (type === 'default') { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.ctrlKey || event.metaKey) && event.key === "k") { + event.preventDefault(); + setIsOpen((open) => !open); } - } - }; + }; - window.addEventListener("keydown", handleNavigation); - return () => { - window.removeEventListener("keydown", handleNavigation); - }; - }, [isOpen, filteredResults, selectedIndex, router]); - - useEffect(() => { - if (itemRefs.current[selectedIndex]) { - itemRefs.current[selectedIndex]?.scrollIntoView({ - behavior: "smooth", - block: "nearest", - }); + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; } - }, [selectedIndex]); + }, [type]); + if (type === "algolia") { + // Just render the component without passing any state props + return ; + } + + // Logic for 'default' search return (
- { - if (!open) setSearchedInput(""); - setIsOpen(open); - }} - > + -
-
-
- -
-
- - -
- - K -
-
-
-
+
- - - Search Documentation - - - Search through the documentation - - setSearchedInput(e.target.value)} - placeholder="Type something to search..." - autoFocus - className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full" - aria-label="Search documentation" - /> - {filteredResults.length == 0 && searchedInput && ( -

- No results found for{" "} - {`"${searchedInput}"`} -

- )} - -
- {filteredResults.map((item, index) => { - const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap; - const paddingClass = paddingMap[level]; - const isActive = index === selectedIndex; - - return ( - - { - 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={0} - > -
1 && "border-l pl-4" - )} - > -
- - {item.title} -
- {isActive && ( -
- Return - -
- )} -
-
-
- ); - })} -
-
- -
- - - - - - -

to navigate

- - - -

to select

- - esc - -

to close

-
-
-
+
); -} - -const paddingMap = { - 1: "pl-2", - 2: "pl-4", - 3: "pl-10", -} as const; +} \ No newline at end of file diff --git a/components/sublink.tsx b/components/sublink.tsx index 68f5897..6b6d493 100644 --- a/components/sublink.tsx +++ b/components/sublink.tsx @@ -32,9 +32,6 @@ export default function SubLink({ // Full path including parent's href const fullHref = `${parentHref}${href}`; - // Check if current path exactly matches this link's href - const isExactActive = useMemo(() => path === fullHref, [path, fullHref]); - // Check if any child is active (for parent items) const hasActiveChild = useMemo(() => { if (!items) return false; @@ -47,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]); @@ -88,7 +86,7 @@ export default function SubLink({
diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx index fa4c740..07479c6 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/toc-observer.tsx b/components/toc-observer.tsx index b0d8a4f..1d595db 100644 --- a/components/toc-observer.tsx +++ b/components/toc-observer.tsx @@ -1,6 +1,5 @@ "use client"; -import { getDocsTocs } from "@/lib/markdown"; import clsx from "clsx"; import Link from "next/link"; import { useState, useRef, useEffect, useCallback } from "react"; @@ -110,7 +109,6 @@ export default function TocObserver({ // Calculate scroll progress for the active section const [scrollProgress, setScrollProgress] = useState(0); - const [activeSectionIndex, setActiveSectionIndex] = useState(0); useEffect(() => { const handleScroll = () => { @@ -137,15 +135,6 @@ export default function TocObserver({ return () => window.removeEventListener('scroll', handleScroll); }, [activeId]); - // Update active section index when activeId changes - useEffect(() => { - if (activeId) { - const index = data.findIndex(item => item.href.slice(1) === activeId); - if (index !== -1) { - setActiveSectionIndex(index); - } - } - }, [activeId, data]); return (
@@ -155,8 +144,9 @@ export default function TocObserver({ const id = href.slice(1); const isActive = activeId === id; const indent = level > 1 ? (level - 1) * 20 : 0; - const isParent = hasChildren(id, level); - const isLastInLevel = index === data.length - 1 || data[index + 1].level <= level; + // Prefix with underscore to indicate intentionally unused + const _isParent = hasChildren(id, level); + const _isLastInLevel = index === data.length - 1 || data[index + 1].level <= level; return (
diff --git a/components/ui/icon-cloud.tsx b/components/ui/icon-cloud.tsx deleted file mode 100644 index c151d60..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, setRotation] = 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/components/ui/input.tsx b/components/ui/input.tsx index 677d05f..aba38dc 100644 --- a/components/ui/input.tsx +++ b/components/ui/input.tsx @@ -2,8 +2,7 @@ import * as React from "react" import { cn } from "@/lib/utils" -export interface InputProps - extends React.InputHTMLAttributes {} +export type InputProps = React.InputHTMLAttributes; const Input = React.forwardRef( ({ className, type, ...props }, ref) => { diff --git a/components/ui/interactive-hover-button.tsx b/components/ui/interactive-hover-button.tsx index 46cf526..e150f87 100644 --- a/components/ui/interactive-hover-button.tsx +++ b/components/ui/interactive-hover-button.tsx @@ -2,8 +2,7 @@ import React from "react"; import { ArrowRight } from "lucide-react"; import { cn } from "@/lib/utils"; -interface InteractiveHoverButtonProps - extends React.ButtonHTMLAttributes {} +type InteractiveHoverButtonProps = React.ButtonHTMLAttributes; export const InteractiveHoverButton = React.forwardRef< HTMLButtonElement, diff --git a/components/ui/table.tsx b/components/ui/table.tsx index 7f3502f..546ddc5 100644 --- a/components/ui/table.tsx +++ b/components/ui/table.tsx @@ -6,10 +6,10 @@ const Table = React.forwardRef< HTMLTableElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
+
@@ -20,7 +20,7 @@ const TableHeader = React.forwardRef< HTMLTableSectionElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( - + )) TableHeader.displayName = "TableHeader" diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx index 8e2a884..d1774e9 100644 --- a/components/ui/tabs.tsx +++ b/components/ui/tabs.tsx @@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef< ( - - {children} - -); - -interface TypingAnimationProps extends MotionProps { - children: string; - className?: string; - duration?: number; - delay?: number; - as?: React.ElementType; -} - -export const TypingAnimation = ({ - children, - className, - duration = 60, - delay = 0, - as: Component = "span", - ...props -}: TypingAnimationProps) => { - if (typeof children !== "string") { - throw new Error("TypingAnimation: children must be a string. Received:"); - } - - const MotionComponent = motion.create(Component, { - forwardMotionProps: true, - }); - - const [displayedText, setDisplayedText] = useState(""); - const [started, setStarted] = useState(false); - const elementRef = useRef(null); - - useEffect(() => { - const startTimeout = setTimeout(() => { - setStarted(true); - }, delay); - return () => clearTimeout(startTimeout); - }, [delay]); - - useEffect(() => { - if (!started) return; - - let i = 0; - const typingEffect = setInterval(() => { - if (i < children.length) { - setDisplayedText(children.substring(0, i + 1)); - i++; - } else { - clearInterval(typingEffect); - } - }, duration); - - return () => { - clearInterval(typingEffect); - }; - }, [children, duration, started]); - - return ( - - {displayedText} - - ); -}; - -interface TerminalProps { - children: React.ReactNode; - className?: string; -} - -export const Terminal = ({ children, className }: TerminalProps) => { - return ( -
-
-
-
-
-
-
-
-
-        {children}
-      
-
- ); -}; 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/lib/markdown.ts b/lib/markdown.ts index f4f4764..35e395f 100644 --- a/lib/markdown.ts +++ b/lib/markdown.ts @@ -8,10 +8,30 @@ import rehypeSlug from "rehype-slug"; import rehypeCodeTitles from "rehype-code-titles"; import { page_routes, ROUTES } from "./routes-config"; import { visit } from "unist-util-visit"; +import type { Node, Parent } from "unist"; import matter from "gray-matter"; +// Type definitions for unist-util-visit +interface Element extends Node { + type: string; + tagName?: string; + properties?: Record & { + className?: string[]; + raw?: string; + }; + children?: Node[]; + value?: string; + raw?: string; // For internal use in processing +} + +interface TextNode extends Node { + type: 'text'; + value: string; +} + // custom components imports import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell } from "@/components/ui/table"; import Pre from "@/components/markdown/PreMdx"; import Note from "@/components/markdown/NoteMdx"; import { Stepper, StepperItem } from "@/components/markdown/StepperMdx"; @@ -27,6 +47,7 @@ import CardGroup from "@/components/markdown/CardGroupMdx"; import Kbd from "@/components/markdown/KeyboardMdx"; import { Release, Changes } from "@/components/markdown/ReleaseMdx"; import { File, Files, Folder } from "@/components/markdown/FileTreeMdx"; +import AccordionGroup from "@/components/markdown/AccordionGroupMdx"; // add custom components const components = { @@ -34,20 +55,22 @@ const components = { TabsContent, TabsList, TabsTrigger, - pre: Pre, - Note, - Stepper, - StepperItem, - img: Image, - a: Link, - Outlet, Youtube, Tooltip, Card, Button, Accordion, + AccordionGroup, CardGroup, Kbd, + // Table Components + table: Table, + thead: TableHeader, + tbody: TableBody, + tfoot: TableFooter, + tr: TableRow, + th: TableHead, + td: TableCell, // Release Note Components Release, Changes, @@ -55,6 +78,56 @@ const components = { File, Files, Folder, + pre: Pre, + Note, + Stepper, + StepperItem, + img: Image, + a: Link, + Outlet, +}; + +// helper function to handle rehype code titles, since by default we can't inject into the className of rehype-code-titles +const handleCodeTitles = () => (tree: Node) => { + visit(tree, "element", (node: Element, index: number | null, parent: Parent | null) => { + // Ensure the visited node is valid + if (!parent || index === null || node.tagName !== 'div') { + return; + } + + // Check if this is the title div from rehype-code-titles + const isTitleDiv = node.properties?.className?.includes('rehype-code-title'); + if (!isTitleDiv) { + return; + } + + // Find the next
 element, skipping over other nodes like whitespace text
+    let nextElement = null;
+    for (let i = index + 1; i < parent.children.length; i++) {
+      const sibling = parent.children[i];
+      if (sibling.type === 'element') {
+        nextElement = sibling as Element;
+        break;
+      }
+    }
+
+    // If the next element is a 
, move the title to it
+    if (nextElement && nextElement.tagName === 'pre') {
+      const titleNode = node.children?.[0] as TextNode;
+      if (titleNode && titleNode.type === 'text') {
+        if (!nextElement.properties) {
+          nextElement.properties = {};
+        }
+        nextElement.properties['data-title'] = titleNode.value;
+
+        // Remove the original title div
+        parent.children.splice(index, 1);
+
+        // Return the same index to continue visiting from the correct position
+        return index;
+      }
+    }
+  });
 };
 
 // can be used for other pages like blogs, Guides etc
@@ -67,6 +140,7 @@ async function parseMdx(rawMdx: string) {
         rehypePlugins: [
           preProcess,
           rehypeCodeTitles,
+          handleCodeTitles,
           rehypePrism,
           rehypeSlug,
           rehypeAutolinkHeadings,
@@ -139,11 +213,11 @@ function justGetFrontmatterFromMD(rawMd: string): Frontmatter {
 }
 
 export async function getAllChilds(pathString: string) {
-  const items = pathString.split("/").filter((it) => it != "");
+  const items = pathString.split("/").filter((it) => it !== "");
   let page_routes_copy = ROUTES;
 
   let prevHref = "";
-  for (let it of items) {
+  for (const it of items) {
     const found = page_routes_copy.find((innerIt) => innerIt.href == `/${it}`);
     if (!found) break;
     prevHref += found.href;
@@ -170,20 +244,28 @@ export async function getAllChilds(pathString: string) {
 }
 
 // for copying the code in pre
-const preProcess = () => (tree: any) => {
-  visit(tree, (node) => {
-    if (node?.type === "element" && node?.tagName === "pre") {
-      const [codeEl] = node.children;
-      if (codeEl.tagName !== "code") return;
-      node.raw = codeEl.children?.[0].value;
+const preProcess = () => (tree: Node) => {
+  visit(tree, (node: Node) => {
+    const element = node as Element;
+    if (element?.type === "element" && element?.tagName === "pre" && element.children) {
+      const [codeEl] = element.children as Element[];
+      if (codeEl.tagName !== "code" || !codeEl.children?.[0]) return;
+
+      const textNode = codeEl.children[0] as TextNode;
+      if (textNode.type === 'text' && textNode.value) {
+        element.raw = textNode.value;
+      }
     }
   });
 };
 
-const postProcess = () => (tree: any) => {
-  visit(tree, "element", (node) => {
-    if (node?.type === "element" && node?.tagName === "pre") {
-      node.properties["raw"] = node.raw;
+const postProcess = () => (tree: Node) => {
+  visit(tree, "element", (node: Node) => {
+    const element = node as Element;
+    if (element?.type === "element" && element?.tagName === "pre") {
+      if (element.properties && element.raw) {
+        element.properties.raw = element.raw;
+      }
     }
   });
 };
diff --git a/package.json b/package.json
index 5f24f90..3b18768 100644
--- a/package.json
+++ b/package.json
@@ -1,59 +1,68 @@
 {
   "name": "docubook",
-  "version": "1.13.6",
+  "version": "2.0.0-beta.3",
   "private": true,
   "scripts": {
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "next lint"
+    "lint": "eslint ."
   },
   "dependencies": {
-    "@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",
-    "class-variance-authority": "^0.7.0",
+    "@docsearch/css": "^3.9.0",
+    "@docsearch/react": "^3.9.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.6",
     "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"
   },
-  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
-}
+  "overrides": {
+    "@types/react": "19.2.8",
+    "@types/react-dom": "19.2.3"
+  },
+  "packageManager": "bun@1.3.8"
+}
\ No newline at end of file
diff --git a/postcss.config.js b/postcss.config.cjs
similarity index 65%
rename from postcss.config.js
rename to postcss.config.cjs
index 12a703d..b4bee66 100644
--- a/postcss.config.js
+++ b/postcss.config.cjs
@@ -1,6 +1,6 @@
 module.exports = {
   plugins: {
-    tailwindcss: {},
+    '@tailwindcss/postcss': {},
     autoprefixer: {},
   },
 };
diff --git a/styles/algolia.css b/styles/algolia.css
new file mode 100644
index 0000000..77b0fad
--- /dev/null
+++ b/styles/algolia.css
@@ -0,0 +1,162 @@
+/*
+================================================================================
+ DocSearch Component Styling (Refactored Version)
+================================================================================
+*/
+
+/* -- LANGKAH 1: Definisi Variabel Global --
+  Variabel tema DocSearch sekarang didefinisikan secara global di :root.
+  Ini menyederhanakan pewarisan tema dan memastikan konsistensi.
+  Mode gelap secara otomatis menimpa variabel ini karena .dark di globals.css.
+*/
+:root {
+  --docsearch-primary-color: hsl(var(--primary));
+  --docsearch-text-color: hsl(var(--muted-foreground));
+  --docsearch-spacing: 12px;
+  --docsearch-icon-stroke-width: 1.4;
+  --docsearch-highlight-color: var(--docsearch-primary-color);
+  --docsearch-muted-color: hsl(var(--muted-foreground));
+  --docsearch-container-background: rgba(0, 0, 0, 0.7);
+  --docsearch-logo-color: hsl(var(--primary-foreground));
+  
+  /* Modal */
+  --docsearch-modal-width: 560px;
+  --docsearch-modal-height: 600px;
+  --docsearch-modal-background: hsl(var(--background));
+  --docsearch-modal-shadow: 0 0 0 1px hsl(var(--border)), 0 8px 20px rgba(0, 0, 0, 0.2);
+  
+  /* SearchBox */
+  --docsearch-searchbox-height: 56px;
+  --docsearch-searchbox-background: hsl(var(--input));
+  --docsearch-searchbox-focus-background: hsl(var(--card));
+  --docsearch-searchbox-shadow: none;
+  
+  /* Hit (Hasil Pencarian) */
+  --docsearch-hit-height: 56px;
+  --docsearch-hit-color: hsl(var(--foreground));
+  --docsearch-hit-active-color: hsl(var(--primary-foreground));
+  --docsearch-hit-background: hsl(var(--card));
+  --docsearch-hit-shadow: none;
+  
+  /* Keys */
+  --docsearch-key-gradient: none;
+  --docsearch-key-shadow: none;
+  --docsearch-key-pressed-shadow: none;
+  
+  /* Footer */
+  --docsearch-footer-height: 44px;
+  --docsearch-footer-background: hsl(var(--background));
+  --docsearch-footer-shadow: none;
+}
+
+/* -- LANGKAH 2: Gaya untuk Tombol Awal --
+  Gaya ini spesifik untuk tombol yang ada di Navbar, 
+  yang dibungkus oleh 
. +*/ +.docsearch .DocSearch-Button { + background-color: hsl(var(--secondary)); + border: 1px solid hsl(var(--border)); + border-radius: 9999px; + width: 160px; + height: 40px; + color: hsl(var(--muted-foreground)); + transition: width 0.3s ease; + margin: 0; +} + +.docsearch .DocSearch-Button:hover { + border-color: var(--docsearch-primary-color); + box-shadow: none; +} + +.docsearch .DocSearch-Search-Icon { + color: var(--docsearch-muted-color); + width: 1rem; + height: 1rem; +} + +.docsearch .DocSearch-Button-Placeholder { + font-style: normal; + margin-left: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + color: var(--docsearch-muted-color); +} + +.docsearch .DocSearch-Button-Key { + background: var(--docsearch-primary-color); + color: var(--docsearch-logo-color); /* Menggunakan variabel yg relevan */ + border-radius: 6px; + font-size: 14px; + font-weight: 500; + height: 24px; + padding: 0 6px; + border: none; + box-shadow: none; + top: 0; +} + +/* -- LANGKAH 3: Gaya untuk Modal dan Isinya -- + Gaya ini menargetkan elemen-elemen modal yang dirender terpisah. + Karena variabel sudah global, kita hanya perlu menata elemennya. +*/ +.DocSearch-Container .DocSearch-Modal { + backdrop-filter: blur(8px); +} + +.DocSearch-Form { + border: 1px solid hsl(var(--border)); + background-color: transparent; +} + +.DocSearch-Input { + font-size: 15px !important; +} + +.DocSearch-Footer { + border-top: 1px solid hsl(var(--border)); +} + +/* Gaya untuk tombol keyboard di footer */ +.DocSearch-Footer--commands kbd { + background-color: hsl(var(--secondary)); + border: 1px solid hsl(var(--border)); + border-bottom-width: 2px; + border-radius: 6px; + color: var(--docsearch-muted-color); + padding: 4px 8px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Menghilangkan gaya default dari ikon di dalam tombol footer */ +.DocSearch-Commands-Key { + background: none; + color: hsl(var(--muted-foreground)); + border: 1px solid hsl(var(--border)); + box-shadow: none; + padding: 2px 4px; + margin-right: 0.4em; + height: 20px; + width: 32px; + border-radius: 6px; +} + +/* -- LANGKAH 4: Gaya Responsif -- + Tidak ada perubahan, hanya mempertahankan fungsionalitas mobile. +*/ +@media (max-width: 768px) { + .docsearch .DocSearch-Button { + width: 40px; + height: 40px; + padding: 0; + justify-content: center; + background: none; + border: none; + } + .docsearch .DocSearch-Button-Placeholder, + .docsearch .DocSearch-Button-Key { + display: none; + } +} \ No newline at end of file diff --git a/styles/globals.css b/styles/globals.css index e9ced65..6565c7c 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,10 +1,131 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +@plugin '@tailwindcss/typography'; -@import url("../styles/syntax.css"); -/* ocean */ +@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 50% 95%; @@ -21,17 +142,18 @@ --muted-foreground: 220 20% 40%; --accent: 132 86% 32%; --accent-foreground: 0 0% 100%; - --destructive: 0 65% 55%; - --destructive-foreground: 220 20% 95%; - --border: 220 15% 90%; - --input: 220 15% 90%; - --ring: 132 86% 42%; + --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 60% 50%; - --chart-2: 220 40% 65%; - --chart-3: 132 86% 42%; - --chart-4: 200 60% 55%; - --chart-5: 240 30% 40%; + --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 { @@ -72,82 +194,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; -} + 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; -} - -.line-clamp-3 { - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; -} - -.line-clamp-2 { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; + .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/styles/syntax.css b/styles/syntax.css index 569e549..8fae5fb 100644 --- a/styles/syntax.css +++ b/styles/syntax.css @@ -1,23 +1,27 @@ /* ocean with green variant */ /* Light Mode */ .keyword { - color: #1EAB18; /* Slightly darker green */ - /* Dark Lime */ + color: #1EAB18; + /* Slightly darker green */ + /* Dark Lime */ } .function { - color: #39D833; /* Brighter lime green */ - /* Bright Lime */ + color: #39D833; + /* Brighter lime green */ + /* Bright Lime */ } .punctuation { - color: #357C30; /* Muted green-gray */ - /* Sage Green */ + color: #357C30; + /* Muted green-gray */ + /* Sage Green */ } .comment { - color: #5F935B; /* Muted green */ - /* Olive Green */ + color: #5F935B; + /* Muted green */ + /* Olive Green */ } .string, @@ -25,34 +29,40 @@ .annotation, .boolean, .number { - color: #2E8F2A; /* Darker green */ - /* Dark Forest Green */ + color: #2E8F2A; + /* Darker green */ + /* Dark Forest Green */ } .tag { - color: #1FC01B; /* Original vibrant green */ - /* Vibrant Green */ + color: #1FC01B; + /* Original vibrant green */ + /* Vibrant Green */ } .attr-name { - color: #4FE34A; /* Light and bright green */ - /* Electric Green */ + color: #4FE34A; + /* Light and bright green */ + /* Electric Green */ } .attr-value { - color: #1EAB18; /* Slightly darker green */ - /* Dark Lime */ + color: #1EAB18; + /* Slightly darker green */ + /* Dark Lime */ } /* Dark Mode */ .dark .keyword { - color: #8CFF7D; /* Soft light green */ - /* Soft Mint */ + color: #8CFF7D; + /* Soft light green */ + /* Soft Mint */ } .dark .function { - color: #A0FF93; /* Light lime green */ - /* Minty Green */ + color: #A0FF93; + /* Light lime green */ + /* Minty Green */ } .dark .string, @@ -60,33 +70,52 @@ .dark .annotation, .dark .boolean, .dark .number { - color: #72FF73; /* Light green */ - /* Spring Green */ + color: #72FF73; + /* Light green */ + /* Spring Green */ } .dark .tag { - color: #7FFF7A; /* Vibrant green */ - /* Neon Green */ + color: #7FFF7A; + /* Vibrant green */ + /* Neon Green */ } .dark .attr-name { - color: #B2FFA3; /* Soft pastel green */ - /* Mint Green */ + color: #B2FFA3; + /* Soft pastel green */ + /* Mint Green */ } .dark .attr-value { - color: #8CFF7D; /* Soft light green */ - /* Soft Mint */ + color: #1EAB18; + /* Slightly darker green */ + /* Dark Lime */ +} + +.dark .comment { + color: #9ca3af; + /* Lighter gray for dark mode */ +} + +.dark .punctuation { + color: #9ca3af; + /* Lighter gray for dark mode */ } .youtube { position: relative; - padding-bottom: 56.25%; /* Rasio aspek 16:9 */ + padding-bottom: 56.25%; + /* Rasio aspek 16:9 */ height: 0; overflow: hidden; - background: #000; /* Latar belakang hitam untuk memadukan player */ - border-radius: 8px; /* Sudut melengkung */ - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); /* Bayangan lembut */ + background: #000; + /* Latar belakang hitam untuk memadukan player */ + border-radius: 8px; + /* Sudut melengkung */ + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + /* Bayangan lembut */ + margin: 24px 0; } .youtube iframe { @@ -96,5 +125,95 @@ width: 100%; height: 100%; border: none; - border-radius: 8px; /* Sudut melengkung pada iframe */ + border-radius: 8px; + /* Sudut melengkung pada iframe */ } + +/* ======================================================================== */ +/* Custom styling for code blocks */ +/* ======================================================================== */ + +.code-block-container { + position: relative; + margin: 1.5rem 0; + border: 1px solid hsl(var(--border)); + overflow: hidden; + font-size: 0.875rem; + border-radius: 0.75rem; +} + +.code-block-header { + display: flex; + align-items: center; + gap: 0.5rem; + background-color: hsl(var(--muted)); + padding: 0.5rem 1rem; + border-bottom: 1px solid hsl(var(--border)); + color: hsl(var(--muted-foreground)); + font-family: monospace; + font-size: 0.8rem; +} + +.code-block-actions { + position: absolute; + top: 0.5rem; + right: 0.75rem; + z-index: 10; +} + +.code-block-actions button { + color: hsl(var(--muted-foreground)); + transition: color 0.2s ease-in-out; +} + +.code-block-actions button:hover { + color: hsl(var(--foreground)); +} + + +.code-block-body pre[class*="language-"] { + margin: 0 !important; + padding: 0 !important; + background: transparent !important; +} + +.line-numbers-wrapper { + position: absolute; + top: 0; + left: 0; + width: 3rem; + padding-top: 1rem; + text-align: right; + color: var(--line-number-color); + user-select: none; +} + +.line-highlight { + position: absolute; + left: 0; + right: 0; + background: hsl(var(--primary) / 0.1); + border-left: 2px solid hsl(var(--primary)); + pointer-events: none; +} + +.code-block-body pre[data-line-numbers="true"] .line-highlight { + padding-left: 3.5rem; +} + +.code-block-body::-webkit-scrollbar { + height: 8px; +} + +.code-block-body::-webkit-scrollbar-track { + background: transparent; +} + +.code-block-body::-webkit-scrollbar-thumb { + background: hsl(var(--border)); + border-radius: 4px; +} + +.code-block-body::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted)); +} \ 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" + ] }