bump docubook version 1.13.6
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
**DocuBook** is a documentation web project designed to provide a simple and user-friendly interface for accessing various types of documentation. This site is crafted for developers and teams who need quick access to references, guides, and essential documents.
|
**DocuBook** is a documentation web project designed to provide a simple and user-friendly interface for accessing various types of documentation. This site is crafted for developers and teams who need quick access to references, guides, and essential documents.
|
||||||
|
|
||||||
[](https://vercel.com/import/project?template=https://github.com/gitfromwildan/docubook)
|
Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/DocuBook/docubook)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|||||||
133
components/context-popover.tsx
Normal file
133
components/context-popover.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { ROUTES, EachRoute } from "@/lib/routes-config";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import * as LucideIcons from "lucide-react";
|
||||||
|
import { ChevronsUpDown, Check, type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface ContextPopoverProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all root-level routes with context
|
||||||
|
function getContextRoutes(): EachRoute[] {
|
||||||
|
return ROUTES.filter(route => route.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first item's href from a route
|
||||||
|
function getFirstItemHref(route: EachRoute): string {
|
||||||
|
return route.items?.[0]?.href ? `${route.href}${route.items[0].href}` : route.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the active context route from the current path
|
||||||
|
function getActiveContextRoute(path: string): EachRoute | undefined {
|
||||||
|
if (!path.startsWith('/docs')) return undefined;
|
||||||
|
const docPath = path.replace(/^\/docs/, '');
|
||||||
|
return getContextRoutes().find(route => docPath.startsWith(route.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get icon component by name
|
||||||
|
function getIcon(name: string) {
|
||||||
|
const Icon = LucideIcons[name as keyof typeof LucideIcons] as LucideIcon | undefined;
|
||||||
|
if (!Icon) return <LucideIcons.FileQuestion className="h-4 w-4" />;
|
||||||
|
return <Icon className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContextPopover({ className }: ContextPopoverProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const [activeRoute, setActiveRoute] = useState<EachRoute>();
|
||||||
|
const contextRoutes = getContextRoutes();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pathname.startsWith("/docs")) {
|
||||||
|
setActiveRoute(getActiveContextRoute(pathname));
|
||||||
|
} else {
|
||||||
|
setActiveRoute(undefined);
|
||||||
|
}
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
if (!pathname.startsWith("/docs") || contextRoutes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className={cn(
|
||||||
|
"w-full max-w-[240px] flex items-center justify-between font-semibold text-foreground px-0 pt-8",
|
||||||
|
"hover:bg-transparent hover:text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{activeRoute?.context?.icon && (
|
||||||
|
<span className="text-primary bg-primary/10 border border-primary dark:border dark:border-accent dark:bg-accent/10 dark:text-accent rounded p-0.5">
|
||||||
|
{getIcon(activeRoute.context.icon)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="truncate text-sm">
|
||||||
|
{activeRoute?.context?.title || activeRoute?.title || 'Select context'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="h-4 w-4 text-foreground/50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-64 p-2"
|
||||||
|
align="start"
|
||||||
|
sideOffset={6}
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{contextRoutes.map((route) => {
|
||||||
|
const isActive = activeRoute?.href === route.href;
|
||||||
|
const firstItemPath = getFirstItemHref(route);
|
||||||
|
const contextPath = `/docs${firstItemPath}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={route.href}
|
||||||
|
onClick={() => router.push(contextPath)}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm",
|
||||||
|
"text-left outline-none transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-primary/20 text-primary dark:bg-accent/20 dark:text-accent"
|
||||||
|
: "text-foreground/80 hover:bg-primary/20 dark:text-foreground/60 dark:hover:bg-accent/20"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{route.context?.icon && (
|
||||||
|
<span className={cn(
|
||||||
|
"flex h-4 w-4 items-center justify-center",
|
||||||
|
isActive ? "text-primary dark:text-accent" : "text-foreground/60"
|
||||||
|
)}>
|
||||||
|
{getIcon(route.context.icon)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0 overflow-hidden">
|
||||||
|
<div className="truncate font-medium">
|
||||||
|
{route.context?.title || route.title}
|
||||||
|
</div>
|
||||||
|
{route.context?.description && (
|
||||||
|
<div className="text-xs text-muted-foreground truncate text-ellipsis overflow-hidden max-w-full">
|
||||||
|
{route.context.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isActive && (
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,44 +1,62 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ROUTES } from "@/lib/routes-config";
|
import { ROUTES, EachRoute } from "@/lib/routes-config";
|
||||||
import SubLink from "./sublink";
|
import SubLink from "./sublink";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface DocsMenuProps {
|
interface DocsMenuProps {
|
||||||
isSheet?: boolean;
|
isSheet?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the current context from the path
|
||||||
|
function getCurrentContext(path: string): string | undefined {
|
||||||
|
if (!path.startsWith('/docs')) return undefined;
|
||||||
|
|
||||||
|
// Extract the first segment after /docs/
|
||||||
|
const match = path.match(/^\/docs\/([^\/]+)/);
|
||||||
|
return match ? match[1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the route that matches the current context
|
||||||
|
function getContextRoute(contextPath: string): EachRoute | undefined {
|
||||||
|
return ROUTES.find(route => {
|
||||||
|
const normalizedHref = route.href.replace(/^\/+|\/+$/g, '');
|
||||||
|
return normalizedHref === contextPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default function DocsMenu({ isSheet = false, className = "" }: DocsMenuProps) {
|
export default function DocsMenu({ isSheet = false, className = "" }: DocsMenuProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
// Skip rendering if not on a docs page
|
// Skip rendering if not on a docs page
|
||||||
if (!pathname.startsWith("/docs")) return null;
|
if (!pathname.startsWith("/docs")) return null;
|
||||||
|
|
||||||
|
// Get the current context
|
||||||
|
const currentContext = getCurrentContext(pathname);
|
||||||
|
|
||||||
|
// Get the route for the current context
|
||||||
|
const contextRoute = currentContext ? getContextRoute(currentContext) : undefined;
|
||||||
|
|
||||||
|
// If no context route is found, don't render anything
|
||||||
|
if (!contextRoute) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
aria-label="Documentation navigation"
|
aria-label="Documentation navigation"
|
||||||
className={className}
|
className={cn("transition-all duration-200", className)}
|
||||||
>
|
>
|
||||||
<ul className="flex flex-col gap-3.5 mt-5 pr-2 pb-6">
|
<ul className="flex flex-col gap-1.5 py-4">
|
||||||
{ROUTES.map((item, index) => {
|
{/* Display only the items from the current context */}
|
||||||
// Normalize href - hapus leading/trailing slashes
|
<li key={contextRoute.title}>
|
||||||
const normalizedHref = `/${item.href.replace(/^\/+|\/+$/g, '')}`;
|
<SubLink
|
||||||
const itemHref = `/docs${normalizedHref}`;
|
{...contextRoute}
|
||||||
|
href={`/docs${contextRoute.href}`}
|
||||||
const modifiedItems = {
|
level={0}
|
||||||
...item,
|
isSheet={isSheet}
|
||||||
href: itemHref,
|
/>
|
||||||
level: 0,
|
</li>
|
||||||
isSheet,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={`${item.title}-${index}`}>
|
|
||||||
<SubLink {...modifiedItems} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,8 +63,9 @@ export function FooterButtons() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label={item.name}
|
aria-label={item.name}
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
<IconComponent className="w-4 h-4 text-gray-800 transition-colors dark:text-gray-400 hover:text-primary" />
|
<IconComponent className="w-4 h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -79,7 +80,8 @@ export function MadeWith() {
|
|||||||
<span className="text-primary">
|
<span className="text-primary">
|
||||||
<Link href="https://www.docubook.pro" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2 text-muted-foreground">
|
<Link href="https://www.docubook.pro" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2 text-muted-foreground">
|
||||||
DocuBook
|
DocuBook
|
||||||
</Link></span>
|
</Link>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import {
|
|||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { Logo, NavMenu } from "@/components/navbar";
|
import { Logo, NavMenu } from "@/components/navbar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlignLeftIcon, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
import { LayoutGrid, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||||
import { FooterButtons } from "@/components/footer";
|
|
||||||
import { DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
import { DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import DocsMenu from "@/components/docs-menu";
|
import DocsMenu from "@/components/docs-menu";
|
||||||
import { ModeToggle } from "@/components/theme-toggle";
|
import { ModeToggle } from "@/components/theme-toggle";
|
||||||
|
import ContextPopover from "@/components/context-popover";
|
||||||
|
|
||||||
// Toggle Button Component
|
// Toggle Button Component
|
||||||
export function ToggleButton({
|
export function ToggleButton({
|
||||||
@@ -52,9 +52,14 @@ export function Leftbar() {
|
|||||||
${collapsed ? "w-[24px]" : "w-[280px]"} flex flex-col pr-2`}
|
${collapsed ? "w-[24px]" : "w-[280px]"} flex flex-col pr-2`}
|
||||||
>
|
>
|
||||||
<ToggleButton collapsed={collapsed} onToggle={toggleCollapse} />
|
<ToggleButton collapsed={collapsed} onToggle={toggleCollapse} />
|
||||||
{/* Scrollable DocsMenu */}
|
{/* Scrollable Content */}
|
||||||
<ScrollArea className="flex-1 px-0.5 pb-4">
|
<ScrollArea className="flex-1 px-0.5 pb-4">
|
||||||
{!collapsed && <DocsMenu />}
|
{!collapsed && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<ContextPopover />
|
||||||
|
<DocsMenu />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
@@ -65,7 +70,7 @@ export function SheetLeftbar() {
|
|||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="max-lg:flex hidden">
|
<Button variant="ghost" size="icon" className="max-lg:flex hidden">
|
||||||
<AlignLeftIcon />
|
<LayoutGrid />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent className="flex flex-col gap-4 px-0" side="left">
|
<SheetContent className="flex flex-col gap-4 px-0" side="left">
|
||||||
@@ -82,7 +87,8 @@ export function SheetLeftbar() {
|
|||||||
<div className="flex flex-col gap-2.5 mt-3 mx-2 px-5">
|
<div className="flex flex-col gap-2.5 mt-3 mx-2 px-5">
|
||||||
<NavMenu isSheet />
|
<NavMenu isSheet />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-2 px-5">
|
<div className="mx-2 px-5 space-y-2">
|
||||||
|
<ContextPopover />
|
||||||
<DocsMenu isSheet />
|
<DocsMenu isSheet />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-2/4 px-5">
|
<div className="flex w-2/4 px-5">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className })
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"grid gap-4",
|
"grid gap-4 text-foreground",
|
||||||
`grid-cols-1 sm:grid-cols-${cols}`,
|
`grid-cols-1 sm:grid-cols-${cols}`,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ const Card: React.FC<CardProps> = ({ title, icon, href, horizontal, children, cl
|
|||||||
const content = (
|
const content = (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"border rounded-lg shadow-sm p-4 transition-all duration-200 bg-white dark:bg-gray-900",
|
"border rounded-lg shadow-sm p-4 transition-all duration-200",
|
||||||
"hover:bg-gray-50 dark:hover:bg-gray-800",
|
"bg-card text-card-foreground border-border",
|
||||||
|
"hover:bg-accent/5 hover:border-accent/30",
|
||||||
"flex gap-2",
|
"flex gap-2",
|
||||||
horizontal ? "flex-row items-center gap-1" : "flex-col space-y-1",
|
horizontal ? "flex-row items-center gap-1" : "flex-col space-y-1",
|
||||||
className
|
className
|
||||||
@@ -29,8 +30,8 @@ const Card: React.FC<CardProps> = ({ title, icon, href, horizontal, children, cl
|
|||||||
>
|
>
|
||||||
{Icon && <Icon className="w-5 h-5 text-primary flex-shrink-0" />}
|
{Icon && <Icon className="w-5 h-5 text-primary flex-shrink-0" />}
|
||||||
<div className="flex-1 min-w-0 my-auto h-full">
|
<div className="flex-1 min-w-0 my-auto h-full">
|
||||||
<span className="text-base font-semibold">{title}</span>
|
<span className="text-base font-semibold text-foreground">{title}</span>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400 -mt-3">{children}</div>
|
<div className="text-sm text-muted-foreground -mt-3">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
138
components/markdown/FileTreeMdx.tsx
Normal file
138
components/markdown/FileTreeMdx.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, ReactNode, Children, isValidElement, cloneElement } from 'react';
|
||||||
|
import { ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen } from 'lucide-react';
|
||||||
|
|
||||||
|
interface FileProps {
|
||||||
|
name: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileComponent = ({ name }: FileProps) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const fileExtension = name.split('.').pop()?.toUpperCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex items-center gap-2 py-1.5 pl-7 pr-3 text-sm rounded-md
|
||||||
|
transition-colors duration-150 cursor-default select-none
|
||||||
|
${isHovered ? 'bg-accent/10' : 'hover:bg-muted/50'}
|
||||||
|
`}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<FileIcon className={`
|
||||||
|
h-3.5 w-3.5 flex-shrink-0 transition-colors
|
||||||
|
${isHovered ? 'text-accent' : 'text-muted-foreground'}
|
||||||
|
`} />
|
||||||
|
<span className="font-mono text-sm text-foreground truncate">{name}</span>
|
||||||
|
{isHovered && fileExtension && (
|
||||||
|
<span className="ml-auto text-xs text-muted-foreground/80">
|
||||||
|
{fileExtension}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FolderComponent = ({ name, children }: FileProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(true); // Set to true by default
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const hasChildren = React.Children.count(children) > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex items-center gap-2 py-1.5 pl-4 pr-3 rounded-md
|
||||||
|
transition-colors duration-150 select-none
|
||||||
|
${isHovered ? 'bg-muted/60' : ''}
|
||||||
|
${isOpen ? 'text-foreground' : 'text-foreground/80'}
|
||||||
|
${hasChildren ? 'cursor-pointer' : 'cursor-default'}
|
||||||
|
`}
|
||||||
|
onClick={() => hasChildren && setIsOpen(!isOpen)}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
tabIndex={-1}
|
||||||
|
onKeyDown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{hasChildren ? (
|
||||||
|
<ChevronRight
|
||||||
|
className={`
|
||||||
|
h-3.5 w-3.5 flex-shrink-0 transition-transform duration-200
|
||||||
|
${isOpen ? 'rotate-90' : ''}
|
||||||
|
${isHovered ? 'text-foreground/70' : 'text-muted-foreground'}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-3.5" />
|
||||||
|
)}
|
||||||
|
{isOpen ? (
|
||||||
|
<FolderOpen className={`
|
||||||
|
h-4 w-4 flex-shrink-0 transition-colors
|
||||||
|
${isHovered ? 'text-accent' : 'text-muted-foreground'}
|
||||||
|
`} />
|
||||||
|
) : (
|
||||||
|
<FolderIcon className={`
|
||||||
|
h-4 w-4 flex-shrink-0 transition-colors
|
||||||
|
${isHovered ? 'text-accent/80' : 'text-muted-foreground/80'}
|
||||||
|
`} />
|
||||||
|
)}
|
||||||
|
<span className={`
|
||||||
|
font-medium transition-colors duration-150
|
||||||
|
${isHovered ? 'text-accent' : ''}
|
||||||
|
`}>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isOpen && hasChildren && (
|
||||||
|
<div className="ml-5 border-l-2 border-muted/50 pl-2">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Files = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
rounded-xl border border-muted/50
|
||||||
|
bg-card/50 backdrop-blur-sm
|
||||||
|
shadow-sm overflow-hidden
|
||||||
|
transition-all duration-200
|
||||||
|
hover:shadow-md hover:border-muted/60
|
||||||
|
"
|
||||||
|
onKeyDown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="p-2">
|
||||||
|
{Children.map(children, (child, index) => {
|
||||||
|
if (isValidElement(child)) {
|
||||||
|
return cloneElement(child, { key: index });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Folder = ({ name, children }: FileProps) => {
|
||||||
|
return <FolderComponent name={name}>{children}</FolderComponent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const File = ({ name }: FileProps) => {
|
||||||
|
return <FileComponent name={name} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// MDX Components
|
||||||
|
export const FileTreeMdx = {
|
||||||
|
Files,
|
||||||
|
File,
|
||||||
|
Folder,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileTreeMdx;
|
||||||
@@ -88,7 +88,7 @@ const KbdComponent: React.FC<KbdProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<kbd
|
<kbd
|
||||||
className="inline-flex items-center justify-center px-2 py-1 mx-0.5 text-xs font-mono font-medium text-gray-800 bg-gray-100 border border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
className="inline-flex items-center justify-center px-2 py-1 mx-0.5 text-xs font-mono font-medium text-foreground bg-secondary/70 border rounded-md"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function Note({
|
|||||||
"dark:bg-stone-950/25 bg-stone-50": type === "note",
|
"dark:bg-stone-950/25 bg-stone-50": type === "note",
|
||||||
"dark:bg-red-950 bg-red-100 border-red-200 dark:border-red-900":
|
"dark:bg-red-950 bg-red-100 border-red-200 dark:border-red-900":
|
||||||
type === "danger",
|
type === "danger",
|
||||||
"dark:bg-orange-950 bg-orange-100 border-orange-200 dark:border-orange-900":
|
"bg-orange-50 border-orange-200 dark:border-orange-900 dark:bg-orange-900/50":
|
||||||
type === "warning",
|
type === "warning",
|
||||||
"dark:bg-green-950 bg-green-100 border-green-200 dark:border-green-900":
|
"dark:bg-green-950 bg-green-100 border-green-200 dark:border-green-900":
|
||||||
type === "success",
|
type === "success",
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ export function Stepper({ children }: PropsWithChildren) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-l pl-9 ml-3 relative",
|
"border-l border-muted pl-9 ml-3 relative",
|
||||||
clsx({
|
clsx({
|
||||||
"pb-5 ": index < length - 1,
|
"pb-5 ": index < length - 1,
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="bg-muted w-8 h-8 text-xs font-medium rounded-md border flex items-center justify-center absolute -left-4 font-code">
|
<div className="bg-muted text-muted-foreground w-8 h-8 text-xs font-medium rounded-md border border-border/50 flex items-center justify-center absolute -left-4 font-code">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
{child}
|
{child}
|
||||||
|
|||||||
@@ -11,14 +11,17 @@ const Tooltip: React.FC<TooltipProps> = ({ text, tip }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="relative inline-block cursor-pointer underline decoration-dotted text-blue-500"
|
className="relative inline-flex items-center cursor-help text-primary hover:text-primary/80 transition-colors"
|
||||||
onMouseEnter={() => setVisible(true)}
|
onMouseEnter={() => setVisible(true)}
|
||||||
onMouseLeave={() => setVisible(false)}
|
onMouseLeave={() => setVisible(false)}
|
||||||
>
|
>
|
||||||
{text}
|
<span className="border-b border-dashed border-primary/60 pb-0.5">
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
{visible && (
|
{visible && (
|
||||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-max max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl bg-background text-foreground text-sm p-2 rounded shadow-md break-words text-center outline outline-1 outline-offset-2">
|
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-64 bg-popover text-popover-foreground text-sm p-3 rounded-md shadow-lg border border-border/50 break-words text-left z-50">
|
||||||
{tip}
|
{tip}
|
||||||
|
<span className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-popover rotate-45 border-b border-r border-border/50 -z-10" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -63,15 +63,15 @@ export function NavMenu({ isSheet = false }) {
|
|||||||
const Comp = (
|
const Comp = (
|
||||||
<Anchor
|
<Anchor
|
||||||
key={`${item.title}-${item.href}`}
|
key={`${item.title}-${item.href}`}
|
||||||
activeClassName="!text-primary md:font-semibold font-medium"
|
activeClassName="text-primary dark:text-accent md:font-semibold font-medium"
|
||||||
absolute
|
absolute
|
||||||
className="flex items-center gap-1 dark:text-stone-300/85 text-stone-800"
|
className="flex items-center gap-1 text-foreground/80 hover:text-foreground transition-colors"
|
||||||
href={item.href}
|
href={item.href}
|
||||||
target={isExternal ? "_blank" : undefined}
|
target={isExternal ? "_blank" : undefined}
|
||||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
{isExternal && <ArrowUpRight className="w-4 h-4 text-muted-foreground" />}
|
{isExternal && <ArrowUpRight className="w-4 h-4 text-foreground/80" />}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
);
|
||||||
return isSheet ? (
|
return isSheet ? (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getPreviousNext } from "@/lib/markdown";
|
import { getPreviousNext } from "@/lib/markdown";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { buttonVariants } from "./ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
export default function Pagination({ pathname }: { pathname: string }) {
|
export default function Pagination({ pathname }: { pathname: string }) {
|
||||||
const res = getPreviousNext(pathname);
|
const res = getPreviousNext(pathname);
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ import {
|
|||||||
import Anchor from "./anchor";
|
import Anchor from "./anchor";
|
||||||
import { advanceSearch, cn } from "@/lib/utils";
|
import { advanceSearch, cn } from "@/lib/utils";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { page_routes } from "@/lib/routes-config";
|
||||||
|
|
||||||
|
// Define the ContextInfo type to match the one in routes-config
|
||||||
|
type ContextInfo = {
|
||||||
|
icon: string;
|
||||||
|
description: string;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchResult = {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
noLink?: boolean;
|
||||||
|
items?: undefined;
|
||||||
|
score?: number;
|
||||||
|
context?: ContextInfo;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Search() {
|
export default function Search() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -39,10 +56,25 @@ export default function Search() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredResults = useMemo(
|
const filteredResults = useMemo<SearchResult[]>(() => {
|
||||||
() => advanceSearch(searchedInput.trim()),
|
const trimmedInput = searchedInput.trim();
|
||||||
[searchedInput]
|
|
||||||
);
|
// 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(() => {
|
useEffect(() => {
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
@@ -100,10 +132,10 @@ export default function Search() {
|
|||||||
<div className="relative flex-1 cursor-pointer max-w-[140px]">
|
<div className="relative flex-1 cursor-pointer max-w-[140px]">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="md:hidden p-2 -ml-2">
|
<div className="md:hidden p-2 -ml-2">
|
||||||
<SearchIcon className="h-5 w-5 text-stone-500 dark:text-stone-400" />
|
<SearchIcon className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block w-full">
|
<div className="hidden md:block w-full">
|
||||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-500 dark:text-stone-400" />
|
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
className="w-full rounded-full dark:bg-background/95 bg-background border h-9 pl-10 pr-0 sm:pr-4 text-sm shadow-sm overflow-ellipsis"
|
className="w-full rounded-full dark:bg-background/95 bg-background border h-9 pl-10 pr-0 sm:pr-4 text-sm shadow-sm overflow-ellipsis"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
|
|||||||
@@ -54,9 +54,10 @@ export default function SubLink({
|
|||||||
// Only apply active styles if it's an exact match and not a parent with active children
|
// Only apply active styles if it's an exact match and not a parent with active children
|
||||||
const Comp = useMemo(() => (
|
const Comp = useMemo(() => (
|
||||||
<Anchor
|
<Anchor
|
||||||
activeClassName={!hasActiveChild ? "text-primary font-medium" : ""}
|
activeClassName={!hasActiveChild ? "dark:text-accent text-primary font-medium" : ""}
|
||||||
href={fullHref}
|
href={fullHref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"text-foreground/80 hover:text-foreground transition-colors",
|
||||||
hasActiveChild && "font-medium text-foreground"
|
hasActiveChild && "font-medium text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -72,8 +73,8 @@ export default function SubLink({
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<h4 className={cn(
|
<h4 className={cn(
|
||||||
"font-medium sm:text-sm",
|
"font-medium sm:text-sm text-foreground/90 hover:text-foreground transition-colors",
|
||||||
hasActiveChild ? "text-foreground" : "text-primary"
|
hasActiveChild ? "text-foreground" : "text-foreground/80"
|
||||||
)}>
|
)}>
|
||||||
{title}
|
{title}
|
||||||
</h4>
|
</h4>
|
||||||
@@ -93,7 +94,7 @@ export default function SubLink({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
{titleOrLink}
|
{titleOrLink}
|
||||||
<span className="ml-2">
|
<span className="ml-2 text-muted-foreground">
|
||||||
{!isOpen ? (
|
{!isOpen ? (
|
||||||
<ChevronRight className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
<ChevronRight className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
||||||
) : (
|
) : (
|
||||||
@@ -111,8 +112,8 @@ export default function SubLink({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start sm:text-sm dark:text-stone-300/85 text-stone-800 ml-0.5 mt-2.5 gap-3",
|
"flex flex-col items-start sm:text-sm text-foreground/80 ml-0.5 mt-2.5 gap-3 hover:[&_a]:text-foreground transition-colors",
|
||||||
level > 0 && "pl-4 border-l ml-1.5"
|
level > 0 && "pl-4 border-l border-border ml-1.5"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{items?.map((innerLink) => (
|
{items?.map((innerLink) => (
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function ModeToggle() {
|
|||||||
setSelectedTheme(value);
|
setSelectedTheme(value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 rounded-full border border-gray-300 dark:border-gray-700 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"
|
||||||
@@ -36,11 +36,11 @@ export function ModeToggle() {
|
|||||||
aria-label="Light Mode"
|
aria-label="Light Mode"
|
||||||
className={`rounded-full p-1 transition-all ${
|
className={`rounded-full p-1 transition-all ${
|
||||||
selectedTheme === "light"
|
selectedTheme === "light"
|
||||||
? "bg-blue-500 text-white"
|
? "bg-primary text-primary-foreground"
|
||||||
: "bg-transparent"
|
: "bg-transparent hover:bg-muted/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Sun className={`h-4 w-4 ${selectedTheme === "light" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
|
<Sun className="h-4 w-4" />
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value="system"
|
value="system"
|
||||||
@@ -48,11 +48,11 @@ export function ModeToggle() {
|
|||||||
aria-label="System Mode"
|
aria-label="System Mode"
|
||||||
className={`rounded-full p-1 transition-all ${
|
className={`rounded-full p-1 transition-all ${
|
||||||
selectedTheme === "system"
|
selectedTheme === "system"
|
||||||
? "bg-blue-500 text-white"
|
? "bg-primary text-primary-foreground"
|
||||||
: "bg-transparent"
|
: "bg-transparent hover:bg-muted/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Monitor className={`h-4 w-4 ${selectedTheme === "system" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
|
<Monitor className="h-4 w-4" />
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value="dark"
|
value="dark"
|
||||||
@@ -60,11 +60,11 @@ export function ModeToggle() {
|
|||||||
aria-label="Dark Mode"
|
aria-label="Dark Mode"
|
||||||
className={`rounded-full p-1 transition-all ${
|
className={`rounded-full p-1 transition-all ${
|
||||||
selectedTheme === "dark"
|
selectedTheme === "dark"
|
||||||
? "bg-blue-500 text-white"
|
? "bg-primary text-primary-foreground"
|
||||||
: "bg-transparent"
|
: "bg-transparent hover:bg-muted/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Moon className={`h-4 w-4 ${selectedTheme === "dark" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
|
<Moon className="h-4 w-4" />
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export default function TocObserver({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative text-sm text-stone-600 dark:text-stone-400">
|
<div className="relative text-sm text-foreground/70 hover:text-foreground transition-colors">
|
||||||
<div className="flex flex-col gap-0">
|
<div className="flex flex-col gap-0">
|
||||||
{data.map(({ href, level, text }, index) => {
|
{data.map(({ href, level, text }, index) => {
|
||||||
const id = href.slice(1);
|
const id = href.slice(1);
|
||||||
@@ -172,7 +172,7 @@ export default function TocObserver({
|
|||||||
{/* Vertical line */}
|
{/* Vertical line */}
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"absolute left-0 top-0 h-full w-px",
|
"absolute left-0 top-0 h-full w-px",
|
||||||
isActive ? "bg-primary/20" : "bg-stone-300 dark:bg-stone-600"
|
isActive ? "bg-primary/20 dark:bg-primary/30" : "bg-border/50 dark:bg-border/50"
|
||||||
)}>
|
)}>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -187,11 +187,11 @@ export default function TocObserver({
|
|||||||
{/* Horizontal line */}
|
{/* Horizontal line */}
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"absolute left-0 top-1/2 h-px w-6",
|
"absolute left-0 top-1/2 h-px w-6",
|
||||||
isActive ? "bg-primary/20" : "bg-stone-300 dark:bg-stone-600"
|
isActive ? "bg-primary/20 dark:bg-primary/30" : "bg-border/50 dark:bg-border/50"
|
||||||
)}>
|
)}>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute left-0 top-0 h-full w-full bg-primary origin-left"
|
className="absolute left-0 top-0 h-full w-full bg-primary dark:bg-accent origin-left"
|
||||||
initial={{ scaleX: 0 }}
|
initial={{ scaleX: 0 }}
|
||||||
animate={{ scaleX: scrollProgress }}
|
animate={{ scaleX: scrollProgress }}
|
||||||
transition={{ duration: 0.3, delay: 0.1 }}
|
transition={{ duration: 0.3, delay: 0.1 }}
|
||||||
@@ -207,8 +207,8 @@ export default function TocObserver({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"relative flex items-center py-2 transition-colors",
|
"relative flex items-center py-2 transition-colors",
|
||||||
{
|
{
|
||||||
"text-primary dark:text-primary-400 font-medium": isActive,
|
"text-primary dark:text-primary font-medium": isActive,
|
||||||
"text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-stone-200": !isActive,
|
"text-muted-foreground hover:text-foreground dark:hover:text-foreground/90": !isActive,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
@@ -229,13 +229,13 @@ export default function TocObserver({
|
|||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"w-1.5 h-1.5 rounded-full transition-all duration-300 relative z-10",
|
"w-1.5 h-1.5 rounded-full transition-all duration-300 relative z-10",
|
||||||
{
|
{
|
||||||
"bg-primary scale-100": isActive,
|
"bg-primary scale-100 dark:bg-primary/90": isActive,
|
||||||
"bg-stone-300 dark:bg-stone-600 scale-75 group-hover:scale-100 group-hover:bg-primary/50": !isActive,
|
"bg-muted-foreground/30 dark:bg-muted-foreground/30 scale-75 group-hover:scale-100 group-hover:bg-primary/50 dark:group-hover:bg-primary/50": !isActive,
|
||||||
}
|
}
|
||||||
)}>
|
)}>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 rounded-full bg-primary/20"
|
className="absolute inset-0 rounded-full bg-primary/20 dark:bg-primary/30"
|
||||||
initial={{ scale: 1 }}
|
initial={{ scale: 1 }}
|
||||||
animate={{ scale: 1.8 }}
|
animate={{ scale: 1.8 }}
|
||||||
transition={{
|
transition={{
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ description: Panduan penggunaan plugin Tutor Addon Tripay
|
|||||||
date: 12-04-2025
|
date: 12-04-2025
|
||||||
---
|
---
|
||||||
|
|
||||||
<Outlet path="getting-started/tutor-tripay" />
|
<Outlet path="plugins/tutor-tripay" />
|
||||||
59
docu.json
59
docu.json
@@ -30,34 +30,51 @@
|
|||||||
"title": "Getting Started",
|
"title": "Getting Started",
|
||||||
"href": "/getting-started",
|
"href": "/getting-started",
|
||||||
"noLink": true,
|
"noLink": true,
|
||||||
|
"context": {
|
||||||
|
"icon": "MousePointerClick",
|
||||||
|
"description": "Tutorial untuk memulai",
|
||||||
|
"title": "Panduan"
|
||||||
|
},
|
||||||
"items": [
|
"items": [
|
||||||
{ "title": "Introduction", "href": "/introduction" },
|
{ "title": "Introduction", "href": "/introduction" },
|
||||||
{ "title": "System Requirements", "href": "/requirements" },
|
{ "title": "System Requirements", "href": "/requirements" },
|
||||||
{ "title": "Installation", "href": "/installation" },
|
{ "title": "Installation", "href": "/installation" },
|
||||||
{ "title": "Activate License", "href": "/license" },
|
{ "title": "Activate License", "href": "/license" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tutor Addons",
|
||||||
|
"href": "/plugins",
|
||||||
|
"noLink": true,
|
||||||
|
"context": {
|
||||||
|
"icon": "Blocks",
|
||||||
|
"description": "Panduan untuk plugins",
|
||||||
|
"title": "Plugins"
|
||||||
|
},
|
||||||
|
"items": [
|
||||||
{
|
{
|
||||||
"title": "Tutor - Tripay",
|
"title": "Tutor - Tripay",
|
||||||
"href": "/tutor-tripay",
|
"href": "/tutor-tripay",
|
||||||
"items": [
|
|
||||||
{ "title": "Daftar Akun", "href": "/mendaftar-akun-tripay" },
|
|
||||||
{ "title": "Sandbox", "href": "/sandbox" },
|
|
||||||
{ "title": "Merchant", "href": "/merchant" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Tutor - Duitku",
|
|
||||||
"href": "/tutor-duitku",
|
|
||||||
"items": [
|
"items": [
|
||||||
{ "title": "Comingsoon", "href": "/#" }
|
{ "title": "Daftar Akun", "href": "/mendaftar-akun-tripay" },
|
||||||
|
{ "title": "Sandbox", "href": "/sandbox" },
|
||||||
|
{ "title": "Merchant", "href": "/merchant" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Tutor - Moota",
|
"title": "Tutor - Duitku",
|
||||||
"href": "/tutor-moota",
|
"href": "/tutor-duitku",
|
||||||
"items": [
|
"items": [
|
||||||
{ "title": "Comingsoon", "href": "/#" }
|
{ "title": "Comingsoon", "href": "/#" }
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"title": "Tutor - Moota",
|
||||||
|
"href": "/tutor-moota",
|
||||||
|
"items": [
|
||||||
|
{ "title": "Comingsoon", "href": "/#" }
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { promises as fs } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
interface ChangelogEntry {
|
|
||||||
version: string;
|
|
||||||
date: string;
|
|
||||||
description?: string;
|
|
||||||
image?: string;
|
|
||||||
changes: {
|
|
||||||
Added?: string[];
|
|
||||||
Improved?: string[];
|
|
||||||
Fixed?: string[];
|
|
||||||
Deprecated?: string[];
|
|
||||||
Removed?: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getChangelogEntries(): Promise<ChangelogEntry[]> {
|
|
||||||
const filePath = path.join(process.cwd(), "CHANGELOG.md");
|
|
||||||
const content = await fs.readFile(filePath, "utf-8");
|
|
||||||
|
|
||||||
const entries: ChangelogEntry[] = [];
|
|
||||||
let currentEntry: Partial<ChangelogEntry> = {};
|
|
||||||
let currentSection: keyof ChangelogEntry["changes"] | null = null;
|
|
||||||
|
|
||||||
const lines = content.split("\n");
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
// Version and date
|
|
||||||
const versionMatch = line.match(/## \[(.+)\] - (\d{4}-\d{2}-\d{2})/);
|
|
||||||
if (versionMatch) {
|
|
||||||
if (Object.keys(currentEntry).length > 0) {
|
|
||||||
entries.push(currentEntry as ChangelogEntry);
|
|
||||||
}
|
|
||||||
currentEntry = {
|
|
||||||
version: versionMatch[1],
|
|
||||||
date: versionMatch[2].split("-").reverse().join("-"),
|
|
||||||
changes: {}
|
|
||||||
};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description
|
|
||||||
if (line.startsWith("> ")) {
|
|
||||||
currentEntry.description = line.slice(2);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image
|
|
||||||
const imageMatch = line.match(/!\[.*\]\((.*)\)/);
|
|
||||||
if (imageMatch) {
|
|
||||||
currentEntry.image = imageMatch[1];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change type
|
|
||||||
const sectionMatch = line.match(/### (Added|Improved|Fixed|Deprecated|Removed)/);
|
|
||||||
if (sectionMatch) {
|
|
||||||
currentSection = sectionMatch[1] as keyof ChangelogEntry["changes"];
|
|
||||||
currentEntry.changes = currentEntry.changes || {};
|
|
||||||
currentEntry.changes[currentSection] = [];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change item
|
|
||||||
if (line.startsWith("- ") && currentSection && currentEntry.changes) {
|
|
||||||
currentEntry.changes[currentSection]?.push(line.slice(2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(currentEntry).length > 0) {
|
|
||||||
entries.push(currentEntry as ChangelogEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,7 @@ import Accordion from "@/components/markdown/AccordionMdx";
|
|||||||
import CardGroup from "@/components/markdown/CardGroupMdx";
|
import CardGroup from "@/components/markdown/CardGroupMdx";
|
||||||
import Kbd from "@/components/markdown/KeyboardMdx";
|
import Kbd from "@/components/markdown/KeyboardMdx";
|
||||||
import { Release, Changes } from "@/components/markdown/ReleaseMdx";
|
import { Release, Changes } from "@/components/markdown/ReleaseMdx";
|
||||||
|
import { File, Files, Folder } from "@/components/markdown/FileTreeMdx";
|
||||||
|
|
||||||
// add custom components
|
// add custom components
|
||||||
const components = {
|
const components = {
|
||||||
@@ -47,8 +48,13 @@ const components = {
|
|||||||
Accordion,
|
Accordion,
|
||||||
CardGroup,
|
CardGroup,
|
||||||
Kbd,
|
Kbd,
|
||||||
|
// Release Note Components
|
||||||
Release,
|
Release,
|
||||||
Changes,
|
Changes,
|
||||||
|
// File Tree Components
|
||||||
|
File,
|
||||||
|
Files,
|
||||||
|
Folder,
|
||||||
};
|
};
|
||||||
|
|
||||||
// can be used for other pages like blogs, Guides etc
|
// can be used for other pages like blogs, Guides etc
|
||||||
@@ -181,64 +187,3 @@ const postProcess = () => (tree: any) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// export type Author = {
|
|
||||||
// avatar?: string;
|
|
||||||
// handle: string;
|
|
||||||
// username: string;
|
|
||||||
// handleUrl: string;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Blog related types and functions have been removed
|
|
||||||
/*
|
|
||||||
export type BlogMdxFrontmatter = BaseMdxFrontmatter & {
|
|
||||||
date: string;
|
|
||||||
authors: Author[];
|
|
||||||
cover: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getAllBlogStaticPaths() {
|
|
||||||
try {
|
|
||||||
const blogFolder = path.join(process.cwd(), "/contents/blogs/");
|
|
||||||
const res = await fs.readdir(blogFolder);
|
|
||||||
return res.map((file) => file.split(".")[0]);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllBlogs() {
|
|
||||||
const blogFolder = path.join(process.cwd(), "/contents/blogs/");
|
|
||||||
const files = await fs.readdir(blogFolder);
|
|
||||||
const uncheckedRes = await Promise.all(
|
|
||||||
files.map(async (file) => {
|
|
||||||
try {
|
|
||||||
const filepath = path.join(process.cwd(), `/contents/blogs/${file}`);
|
|
||||||
const rawMdx = await fs.readFile(filepath, "utf-8");
|
|
||||||
return {
|
|
||||||
...justGetFrontmatterFromMD<BlogMdxFrontmatter>(rawMdx),
|
|
||||||
slug: file.split(".")[0],
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return uncheckedRes.filter((it) => !!it) as (BlogMdxFrontmatter & {
|
|
||||||
slug: string;
|
|
||||||
})[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getBlogForSlug(slug: string) {
|
|
||||||
try {
|
|
||||||
const blogFile = path.join(process.cwd(), "/contents/blogs/", `${slug}.mdx`);
|
|
||||||
const rawMdx = await fs.readFile(blogFile, "utf-8");
|
|
||||||
return await parseMdx<BlogMdxFrontmatter>(rawMdx);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import docuConfig from "@/docu.json"; // Import JSON file
|
import docuConfig from "@/docu.json"; // Import JSON file
|
||||||
|
|
||||||
|
export type ContextInfo = {
|
||||||
|
icon: string;
|
||||||
|
description: string;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type EachRoute = {
|
export type EachRoute = {
|
||||||
title: string;
|
title: string;
|
||||||
href: string;
|
href: string;
|
||||||
noLink?: boolean; // Sekarang mendukung boolean
|
noLink?: boolean;
|
||||||
|
context?: ContextInfo;
|
||||||
items?: EachRoute[];
|
items?: EachRoute[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
8628
package-lock.json
generated
8628
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "docubook",
|
"name": "docubook",
|
||||||
"version": "1.11.0",
|
"version": "1.13.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -55,5 +55,5 @@
|
|||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.10",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"packageManager": "npm@11.3.0"
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
|
||||||
<title>Hire me 🚀</title>
|
|
||||||
<script async src="https://tally.so/widgets/embed.js"></script>
|
|
||||||
<style type="text/css">
|
|
||||||
html { margin: 0; height: 100%; overflow: hidden; }
|
|
||||||
iframe { position: absolute; top: 0; right: 0; bottom: 0; left: 0; border: 0; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<iframe data-tally-src="https://tally.so/r/mZPQ4e" width="100%" height="100%" frameborder="0" marginheight="0" marginwidth="0" title="Hire me 🚀"></iframe>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@@ -1,57 +0,0 @@
|
|||||||
/* GitHub-style editor customizations */
|
|
||||||
.editor-container {
|
|
||||||
@apply relative font-mono text-sm leading-relaxed;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-textarea {
|
|
||||||
@apply w-full h-full min-h-[600px] p-4 pl-14 bg-background resize-none focus:outline-none;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
tab-size: 2;
|
|
||||||
counter-reset: line;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Line numbers */
|
|
||||||
.editor-line-numbers {
|
|
||||||
@apply absolute left-0 top-0 bottom-0 w-10 bg-muted/30 border-r select-none;
|
|
||||||
padding: 1rem 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-line-numbers-content {
|
|
||||||
@apply text-right pr-2 text-muted-foreground/70;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-line-numbers-content > div {
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-line-numbers-content > div::before {
|
|
||||||
content: attr(data-line-number);
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Selection styling */
|
|
||||||
.editor-textarea::selection {
|
|
||||||
@apply bg-primary/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .editor-textarea::selection {
|
|
||||||
@apply bg-primary/30;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar styling */
|
|
||||||
.editor-textarea::-webkit-scrollbar {
|
|
||||||
@apply w-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-textarea::-webkit-scrollbar-track {
|
|
||||||
@apply bg-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-textarea::-webkit-scrollbar-thumb {
|
|
||||||
@apply bg-muted-foreground/20 rounded-full hover:bg-muted-foreground/30;
|
|
||||||
}
|
|
||||||
@@ -3,65 +3,64 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import url("../styles/syntax.css");
|
@import url("../styles/syntax.css");
|
||||||
@import url("../styles/editor.css");
|
|
||||||
|
|
||||||
/* ocean */
|
/* Modern Blue Theme with Primary #0353D3 */
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 210 60% 97%; /* Lighter sky blue */
|
--background: 210 40% 98%;
|
||||||
--foreground: 220 30% 10%; /* Deep navy */
|
--foreground: 220 30% 15%;
|
||||||
--card: 210 50% 99%; /* Almost white blue */
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 220 30% 10%;
|
--card-foreground: 220 30% 15%;
|
||||||
--popover: 210 50% 99%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 220 30% 10%;
|
--popover-foreground: 220 30% 15%;
|
||||||
--primary: 220 85% 55%; /* Vibrant azure blue */
|
--primary: 215 100% 42%; /* #0353D3 */
|
||||||
--primary-foreground: 210 60% 97%;
|
--primary-foreground: 0 0% 100%;
|
||||||
--secondary: 220 40% 80%; /* Softer sky blue */
|
--secondary: 215 30% 90%;
|
||||||
--secondary-foreground: 220 30% 10%;
|
--secondary-foreground: 220 30% 15%;
|
||||||
--muted: 220 40% 80%;
|
--muted: 215 20% 92%;
|
||||||
--muted-foreground: 220 30% 30%; /* Deeper steel blue */
|
--muted-foreground: 220 15% 50%;
|
||||||
--accent: 200 75% 38%; /* Stronger ocean blue */
|
--accent: 215 100% 35%;
|
||||||
--accent-foreground: 0 0% 100%; /* Pure white */
|
--accent-foreground: 0 0% 100%;
|
||||||
--destructive: 0 70% 50%; /* More vivid red */
|
--destructive: 0 85% 60%;
|
||||||
--destructive-foreground: 220 30% 95%; /* Lightened foreground */
|
--destructive-foreground: 0 0% 100%;
|
||||||
--border: 220 20% 85%; /* Slightly darker grey-blue */
|
--border: 215 20% 85%;
|
||||||
--input: 220 20% 85%;
|
--input: 215 20% 85%;
|
||||||
--ring: 220 50% 50%; /* More noticeable blue ring */
|
--ring: 215 100% 42%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--chart-1: 210 65% 45%; /* Classic blue */
|
--chart-1: 215 100% 42%;
|
||||||
--chart-2: 220 45% 60%; /* Softer sky */
|
--chart-2: 200 100% 40%;
|
||||||
--chart-3: 220 75% 45%; /* Azure blue */
|
--chart-3: 220 76% 60%;
|
||||||
--chart-4: 200 65% 50%; /* Ocean blue */
|
--chart-4: 190 90% 50%;
|
||||||
--chart-5: 240 35% 35%; /* Deeper teal */
|
--chart-5: 230 86% 45%;
|
||||||
--line-number-color: rgba(0, 0, 0, 0.05);
|
--line-number-color: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 220 20% 8%; /* Deeper midnight navy */
|
--background: 220 25% 8%;
|
||||||
--foreground: 220 85% 92%; /* Brighter sky blue */
|
--foreground: 210 30% 98%;
|
||||||
--card: 220 20% 10%; /* Slightly darker midnight */
|
--card: 220 25% 12%;
|
||||||
--card-foreground: 220 85% 92%;
|
--card-foreground: 210 30% 98%;
|
||||||
--popover: 220 20% 10%;
|
--popover: 220 25% 12%;
|
||||||
--popover-foreground: 220 85% 92%;
|
--popover-foreground: 210 30% 98%;
|
||||||
--primary: 210 75% 65%; /* Softer but bright blue */
|
--primary: 217 91% 60%; /* Slightly adjusted for better visibility */
|
||||||
--primary-foreground: 220 20% 8%;
|
--primary-foreground: 0 0% 100%;
|
||||||
--secondary: 220 35% 12%; /* Darker steel blue */
|
--secondary: 215 25% 18%;
|
||||||
--secondary-foreground: 220 85% 92%;
|
--secondary-foreground: 210 30% 98%;
|
||||||
--muted: 220 35% 12%;
|
--muted: 215 20% 22%;
|
||||||
--muted-foreground: 210 25% 80%; /* Pale navy */
|
--muted-foreground: 215 20% 75%;
|
||||||
--accent: 220 85% 55%; /* Vibrant azure blue */
|
--accent: 215 100% 60%;
|
||||||
--accent-foreground: 220 85% 92%;
|
--accent-foreground: 0 0% 100%;
|
||||||
--destructive: 0 75% 50%; /* More noticeable red */
|
--destructive: 0 85% 65%;
|
||||||
--destructive-foreground: 220 85% 92%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
--border: 220 35% 12%; /* Darker steel blue */
|
--border: 215 20% 28%;
|
||||||
--input: 220 35% 12%;
|
--input: 215 20% 22%;
|
||||||
--ring: 220 65% 55%; /* Vivid blue ring */
|
--ring: 217 91% 60%;
|
||||||
--chart-1: 210 65% 45%; /* Classic blue */
|
--chart-1: 217 91% 60%;
|
||||||
--chart-2: 220 45% 60%; /* Softer sky */
|
--chart-2: 200 100% 60%;
|
||||||
--chart-3: 220 75% 45%; /* Azure blue */
|
--chart-3: 220 90% 70%;
|
||||||
--chart-4: 200 65% 50%; /* Ocean blue */
|
--chart-4: 190 100% 65%;
|
||||||
--chart-5: 240 35% 35%; /* Deeper teal */
|
--chart-5: 230 90% 60%;
|
||||||
--line-number-color: rgba(255, 255, 255, 0.05);
|
--line-number-color: rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,22 +118,6 @@ pre>code {
|
|||||||
background-color: transparent !important;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.animate-shine {
|
.animate-shine {
|
||||||
--animate-shine: shine var(--duration) infinite linear;
|
--animate-shine: shine var(--duration) infinite linear;
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/* ocean */
|
/* Modern Blue Theme with Primary #0353D3 */
|
||||||
/* Light Mode */
|
/* Light Mode */
|
||||||
.keyword {
|
.keyword {
|
||||||
color: #2563eb;
|
color: #1e40af; /* Darker blue for better contrast */
|
||||||
/* Vibrant Blue */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.function {
|
.function {
|
||||||
color: #0284c7;
|
color: #0369a1; /* Deep blue */
|
||||||
/* Deep Sky Blue */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.punctuation {
|
.punctuation {
|
||||||
color: #475569;
|
color: #4b5563; /* Slate gray */
|
||||||
/* Cool Slate Gray */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
color: #64748b;
|
color: #6b7280; /* Muted gray */
|
||||||
/* Muted Slate */
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.string,
|
.string,
|
||||||
@@ -25,34 +22,29 @@
|
|||||||
.annotation,
|
.annotation,
|
||||||
.boolean,
|
.boolean,
|
||||||
.number {
|
.number {
|
||||||
color: #0369a1;
|
color: #0d9488; /* Teal for better distinction */
|
||||||
/* Dark Cyan */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
color: #1e40af;
|
color: #1e40af; /* Matching keyword color */
|
||||||
/* Indigo */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attr-name {
|
.attr-name {
|
||||||
color: #0ea5e9;
|
color: #1d4ed8; /* Slightly lighter blue */
|
||||||
/* Light Sky Blue */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attr-value {
|
.attr-value {
|
||||||
color: #2563eb;
|
color: #2563eb; /* Primary blue */
|
||||||
/* Bright Blue */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode */
|
/* Dark Mode */
|
||||||
.dark .keyword {
|
.dark .keyword {
|
||||||
color: #93c5fd;
|
color: #60a5fa; /* Soft blue - good contrast on dark */
|
||||||
/* Soft Blue */
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .function {
|
.dark .function {
|
||||||
color: #38bdf8;
|
color: #38bdf8; /* Sky blue */
|
||||||
/* Sky Blue */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .string,
|
.dark .string,
|
||||||
@@ -60,23 +52,37 @@
|
|||||||
.dark .annotation,
|
.dark .annotation,
|
||||||
.dark .boolean,
|
.dark .boolean,
|
||||||
.dark .number {
|
.dark .number {
|
||||||
color: #60a5fa;
|
color: #2dd4bf; /* Teal - good visibility */
|
||||||
/* Light Blue */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .tag {
|
.dark .tag {
|
||||||
color: #3b82f6;
|
color: #60a5fa; /* Matching keyword color */
|
||||||
/* Bold Blue */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .attr-name {
|
.dark .attr-name {
|
||||||
color: #67e8f9;
|
color: #7dd3fc; /* Lighter blue - better visibility */
|
||||||
/* Aqua */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .attr-value {
|
.dark .attr-value {
|
||||||
color: #93c5fd;
|
color: #60a5ff; /* Brighter blue - better visibility */
|
||||||
/* Frosty Blue */
|
}
|
||||||
|
|
||||||
|
.dark .comment {
|
||||||
|
color: #94a3b8; /* Light gray - better contrast for dark mode */
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .punctuation {
|
||||||
|
color: #cbd5e1; /* Lighter gray - better visibility */
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .operator {
|
||||||
|
color: #cbd5e1; /* Light gray for operators */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .selector {
|
||||||
|
color: #f472b6; /* Pink for selectors - better visibility */
|
||||||
}
|
}
|
||||||
|
|
||||||
.youtube {
|
.youtube {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const config = {
|
|||||||
"./src/**/*.{ts,tsx}",
|
"./src/**/*.{ts,tsx}",
|
||||||
],
|
],
|
||||||
prefix: "",
|
prefix: "",
|
||||||
safelist: ["line-clamp-3","line-clam-2"],
|
|
||||||
theme: {
|
theme: {
|
||||||
container: {
|
container: {
|
||||||
center: true,
|
center: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user