push docu version 1.13.6

This commit is contained in:
gitfromwildan
2025-06-01 22:19:56 +07:00
parent 3f6a407a30
commit 9426e94884
28 changed files with 437 additions and 9261 deletions

View File

@@ -1,357 +0,0 @@
## [1.8.5] - 2025-05-10
> Add sponsor card on single docs page
### Added
- Expandables Leftbar
- sponsor badges or ads
- boolean show/hide `edit on github`
- with the same code run anywhere (bun or nodejs)
- add fronmatter (metadata) to playground editor
### Improved
- adjusment docu.json
- adjustment navbar, footer and components
### Fixed
- bun compability rename .js to common js
- cli manage packageManager on package.json
- inconsistent design moved to better UI/UX
- error handle render footer.social
### Removed
- remove confused and verbose cli on installer
## [1.8.0] - 2025-03-01
> Now looks more modern and clean which is a big change in layout and design
### Added
- Social footer
- Toggle group
- Site description {meta.description} in footer
- Site title {meta.title} in footer
### Improved
- Header design changes
- Footer design changes
- New functions in theme provider
- Object changes in docu.json
### Fixed
- Updates to path structure components
- Groups to organize components
## [1.7.0] - 2025-02-23
> Remove the old function in the search dialog and replace it with a new and more optimal feature
### Added
- Up and down navigation : search dialog.tsx
- Enter (return) to select : search dialog.tsx
- Escape to close the dialog : search dialog.tsx
### Improved
- Maintenance for anchor components
- Anchor.tsx adjustments for all elements that use it
### Removed
- Remove suboptimal search features
## [1.6.0] - 2025-02-21
> New Feature Card Groups with arrays for more Flexible Content
### Added
- Card Groups Components
- Props : href to url link
- Props : horizontal boolean
### Improved
- Card props styling
- Compability for Cards components
- {children} support for card content
### Removed
- remove unused props cards components
## [1.5.0] - 2025-02-18
> Minor Update - improved features and responsiveness on all devices
### Added
- New dialog footer on searchbox above @media 768px
- Icon X for close dialog on searcbox as esc on medium screen
### Improved
- Responsive Leftbar components on large screen
- Menu Trigger on medium screen
- Responsive Navbar components on medium screen
- Better UX for searchbox dialog
- tooltips components can be written together with regular paragraphs
### Fixed
- Responsive issue
- Compatibility for Bun
- Changes postcss.config.js to .cjs for Bun
- all CLI installer and updater not working
- adjustments for package managers npm, pnpm, bun, yarn
## [1.4.2] - 2025-02-16
> Complex Content for Accordion Component props {children}
### Added
- New Props with {children} in accordion
- Compatibility for markdown in accordion
- Nested components inside an accordion
- New icon on note components
- add CLI npx @docubook/create@latest
- add CLI npx @docubook/update@latest
### Improved
- Better UI design for accordion
- Styling Note components on markdown
- Change accordion output on playground
- Change accordion output on snippet
### Removed
- Remove depcreated props on accordion
- Remove CLI npx update_docu
- Remove CLI npx create_docu
## [1.4.0] - 2025-02-11
> Floating Button Version with Dynamic Tag {version} on Changelog page
### Added
- New components / changelog floating-version.tsx
- Button popover to open version-toc below @media 1024px
- Dynamic tag by section ID #version
- Dynamic url tag #version
- Dynamic version indicator on floating version when scrolling section by ID
### Improved
- change icon version history
- responsive version-toc
- improvement components to changelog page
## [1.3.8] - 2025-02-08
> Responsive Table of Content
### Added
- Components terminal MagicUI
- Components card Shadcn
- New mob-toc for a better experience on mobile devices
- New Components scroll to top button
- Scroll to top :blog-post
- Scroll to top :docs-post
### Improved
- lib/markdown for generated dynamic toc on markdown
- Responsive Table of Content below @media 1024px
- Improve docs page
## [1.3.6] - 2025-02-01
> Appears more modern editor for Docu Play
![version 1.3.6 - Playground](https://docubook.pro/images/new-editor.png)
### Added
- Line Number for editor
- editor.css
### Improved
- Better Design for Editor
- Similar to Github Editor
- Moved Handler Element (copy, download, reset and fullscreen) on Header
## [1.3.5] - 2025-01-30
> it's Easy to Write Markdown with Playground
![version 1.3.5 - Playground](https://docubook.pro/images/img-playground.png)
### Added
- New Playground Page
- New Playground Layout
- Toolbar for Markdown Components
- Fullscreen Mode to Focus Editing Your Content
- Copy to Clipboard Your Content
- Download Your Content as index.mdx
- Reset Your Content without refresh the Browser
- Only Large Screen for Better Experience
## [1.3.1] - 2025-01-20
> Snippet Feature to Easily Write Markdown and Call DocuBook Components
![version 1.3.1 - Snippet Features](https://docubook.pro/images/snippet.png)
### Added
- New Feature Snippet for Markdown Components
- Support Snippet for Visual Studio Code
### Removed
- remove props icon and props description for accordion components
## [1.3.0] - 2024-12-31
> Release Note Feature to Make it Easier to Write Changelogs
### Added
- New Release Note Feature
- New Layout for Changelog page
- New Changelog page
- Add Release Note Component
- Easily write release notes directly from the CHANGELOG.md file
- TOC for versioning
- Write with the markdown tag
- Add lib / changelog.ts
### Improved
- Improvement Responsive feature image for Version Entry
- Improvement Layout for changelog page
- Improvement Padding on mobile devices
- Only use containers of md size
- Improvement syntax.css for ul>li classes
### Fixed
- Fix og:image not showing on Page.tsx
- Fix text-indent on class li
### Removed
- Remove excessive padding
- Remove Logo on Footer
## [1.2.0] - 2024-12-22
> New Accordion Component : Support content plain text, html and all markdown component
### Added
- add New Accordion
### Improved
- Props Improvement
- Support Dynamic Content for Accordion
## [1.1.0] - 2024-12-15
> Minor Update : Easily manage set up with docu.json
### Added
- add docu.json file
- add openGraph (title, description, image)
- add Dynamic metadata
- Generate metadata as openGraph
- openGraph support for .mdx
### Improved
- routes-config from json
- Frontmatter improvement
- Edit the content of footer.tsx simply via the docu.json file
- Edit the content of navbar.tsx simply via the docu.json file
## [1.0.7] - 2024-12-14
> Easily updates your DocuBook Version with CLI npx update_docu
### Added
- CLI npx update_docu (update features into docubook existing directory)
- Playground (easily to written content)
- New Button component
- Navbar external link conditions
- CLI npx create_docu
### Improved
- Searchbar Improvement
- Navigation Improvement
- Edit on Github Improvement
### Removed
- Remove CLI npx create-docu (on this version not usage dash `-`)
## [1.0.6] - 2024-11-24
> New Components, Fix and Improvement
### Added
- New Card component
- New Tooltips component
### Fixed
- change root folder
### Improved
- logo on navbar & footer
- easily change logo
## [1.0.5] - 2024-11-16
> Add New Features and Improvement for this version
### Added
- New Youtube component
- edit this page - easily manage directory content via the github repo
- support installation via cli commant npx create-docu
### Improved
- keyboard shortcut command + k or ctrl + k to open search dialog
## [1.0.0] - 2024-11-10
> Initial release of DocuBook to create interactive nested docs with MDX
### Added
- Initial release of DocuBook
- Basic documentation structure
- Markdown support with MDX
- Responsive design
- Search functionality
- Dark mode support

View File

@@ -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.
[![Deploy with
Vercel](https://vercel.com/button)](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

View 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>
);
}

View File

@@ -1,44 +1,62 @@
"use client";
import { ROUTES } from "@/lib/routes-config";
import { ROUTES, EachRoute } from "@/lib/routes-config";
import SubLink from "./sublink";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
interface DocsMenuProps {
isSheet?: boolean;
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) {
const pathname = usePathname();
// Skip rendering if not on a docs page
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 (
<nav
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">
{ROUTES.map((item, index) => {
// Normalize href - hapus leading/trailing slashes
const normalizedHref = `/${item.href.replace(/^\/+|\/+$/g, '')}`;
const itemHref = `/docs${normalizedHref}`;
const modifiedItems = {
...item,
href: itemHref,
level: 0,
isSheet,
};
return (
<li key={`${item.title}-${index}`}>
<SubLink {...modifiedItems} />
<ul className="flex flex-col gap-1.5 py-4">
{/* Display only the items from the current context */}
<li key={contextRoute.title}>
<SubLink
{...contextRoute}
href={`/docs${contextRoute.href}`}
level={0}
isSheet={isSheet}
/>
</li>
);
})}
</ul>
</nav>
);

View File

@@ -63,8 +63,9 @@ export function FooterButtons() {
target="_blank"
rel="noopener noreferrer"
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>
);
})}
@@ -79,7 +80,8 @@ export function MadeWith() {
<span className="text-primary">
<Link href="https://www.docubook.pro" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2 text-muted-foreground">
DocuBook
</Link></span>
</Link>
</span>
</>
);
}

View File

@@ -9,12 +9,12 @@ import {
} from "@/components/ui/sheet";
import { Logo, NavMenu } from "@/components/navbar";
import { Button } from "@/components/ui/button";
import { AlignLeftIcon, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { FooterButtons } from "@/components/footer";
import { LayoutGrid, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import DocsMenu from "@/components/docs-menu";
import { ModeToggle } from "@/components/theme-toggle";
import ContextPopover from "@/components/context-popover";
// Toggle Button Component
export function ToggleButton({
@@ -52,9 +52,14 @@ export function Leftbar() {
${collapsed ? "w-[24px]" : "w-[280px]"} flex flex-col pr-2`}
>
<ToggleButton collapsed={collapsed} onToggle={toggleCollapse} />
{/* Scrollable DocsMenu */}
{/* Scrollable Content */}
<ScrollArea className="flex-1 px-0.5 pb-4">
{!collapsed && <DocsMenu />}
{!collapsed && (
<div className="space-y-2">
<ContextPopover />
<DocsMenu />
</div>
)}
</ScrollArea>
</aside>
);
@@ -65,7 +70,7 @@ export function SheetLeftbar() {
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="max-lg:flex hidden">
<AlignLeftIcon />
<LayoutGrid />
</Button>
</SheetTrigger>
<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">
<NavMenu isSheet />
</div>
<div className="mx-2 px-5">
<div className="mx-2 px-5 space-y-2">
<ContextPopover />
<DocsMenu isSheet />
</div>
<div className="flex w-2/4 px-5">

View File

@@ -13,7 +13,7 @@ const CardGroup: React.FC<CardGroupProps> = ({ children, cols = 2, className })
return (
<div
className={clsx(
"grid gap-4",
"grid gap-4 text-foreground",
`grid-cols-1 sm:grid-cols-${cols}`,
className
)}

View File

@@ -20,8 +20,9 @@ const Card: React.FC<CardProps> = ({ title, icon, href, horizontal, children, cl
const content = (
<div
className={clsx(
"border rounded-lg shadow-sm p-4 transition-all duration-200 bg-white dark:bg-gray-900",
"hover:bg-gray-50 dark:hover:bg-gray-800",
"border rounded-lg shadow-sm p-4 transition-all duration-200",
"bg-card text-card-foreground border-border",
"hover:bg-accent/5 hover:border-accent/30",
"flex gap-2",
horizontal ? "flex-row items-center gap-1" : "flex-col space-y-1",
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" />}
<div className="flex-1 min-w-0 my-auto h-full">
<span className="text-base font-semibold">{title}</span>
<div className="text-sm text-gray-600 dark:text-gray-400 -mt-3">{children}</div>
<span className="text-base font-semibold text-foreground">{title}</span>
<div className="text-sm text-muted-foreground -mt-3">{children}</div>
</div>
</div>
);

View 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;

View File

@@ -88,7 +88,7 @@ const KbdComponent: React.FC<KbdProps> = ({
return (
<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}
>
{renderContent()}

View File

@@ -29,7 +29,7 @@ export default function 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":
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",
"dark:bg-green-950 bg-green-100 border-green-200 dark:border-green-900":
type === "success",

View File

@@ -11,13 +11,13 @@ export function Stepper({ children }: PropsWithChildren) {
return (
<div
className={cn(
"border-l pl-9 ml-3 relative",
"border-l border-muted pl-9 ml-3 relative",
clsx({
"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}
</div>
{child}

View File

@@ -11,14 +11,17 @@ const Tooltip: React.FC<TooltipProps> = ({ text, tip }) => {
return (
<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)}
onMouseLeave={() => setVisible(false)}
>
<span className="border-b border-dashed border-primary/60 pb-0.5">
{text}
</span>
{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}
<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>

View File

@@ -63,15 +63,15 @@ export function NavMenu({ isSheet = false }) {
const Comp = (
<Anchor
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
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}
target={isExternal ? "_blank" : undefined}
rel={isExternal ? "noopener noreferrer" : undefined}
>
{item.title}
{isExternal && <ArrowUpRight className="w-4 h-4 text-muted-foreground" />}
{isExternal && <ArrowUpRight className="w-4 h-4 text-foreground/80" />}
</Anchor>
);
return isSheet ? (

View File

@@ -1,7 +1,7 @@
import { getPreviousNext } from "@/lib/markdown";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import Link from "next/link";
import { buttonVariants } from "./ui/button";
import { buttonVariants } from "@/components/ui/button";
export default function Pagination({ pathname }: { pathname: string }) {
const res = getPreviousNext(pathname);

View File

@@ -17,6 +17,23 @@ import {
import Anchor from "./anchor";
import { advanceSearch, cn } from "@/lib/utils";
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() {
const router = useRouter();
@@ -39,10 +56,25 @@ export default function Search() {
};
}, []);
const filteredResults = useMemo(
() => advanceSearch(searchedInput.trim()),
[searchedInput]
);
const filteredResults = useMemo<SearchResult[]>(() => {
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);
@@ -100,10 +132,10 @@ export default function Search() {
<div className="relative flex-1 cursor-pointer max-w-[140px]">
<div className="flex items-center">
<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 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
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"

View File

@@ -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
const Comp = useMemo(() => (
<Anchor
activeClassName={!hasActiveChild ? "text-primary font-medium" : ""}
activeClassName={!hasActiveChild ? "dark:text-accent text-primary font-medium" : ""}
href={fullHref}
className={cn(
"text-foreground/80 hover:text-foreground transition-colors",
hasActiveChild && "font-medium text-foreground"
)}
>
@@ -72,8 +73,8 @@ export default function SubLink({
)
) : (
<h4 className={cn(
"font-medium sm:text-sm",
hasActiveChild ? "text-foreground" : "text-primary"
"font-medium sm:text-sm text-foreground/90 hover:text-foreground transition-colors",
hasActiveChild ? "text-foreground" : "text-foreground/80"
)}>
{title}
</h4>
@@ -93,7 +94,7 @@ export default function SubLink({
>
<div className="flex items-center justify-between w-full">
{titleOrLink}
<span className="ml-2">
<span className="ml-2 text-muted-foreground">
{!isOpen ? (
<ChevronRight className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
) : (
@@ -111,8 +112,8 @@ export default function SubLink({
>
<div
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",
level > 0 && "pl-4 border-l ml-1.5"
"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 border-border ml-1.5"
)}
>
{items?.map((innerLink) => (

View File

@@ -28,7 +28,7 @@ export function ModeToggle() {
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
value="light"
@@ -36,11 +36,11 @@ export function ModeToggle() {
aria-label="Light Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "light"
? "bg-blue-500 text-white"
: "bg-transparent"
? "bg-primary text-primary-foreground"
: "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
value="system"
@@ -48,11 +48,11 @@ export function ModeToggle() {
aria-label="System Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "system"
? "bg-blue-500 text-white"
: "bg-transparent"
? "bg-primary text-primary-foreground"
: "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
value="dark"
@@ -60,11 +60,11 @@ export function ModeToggle() {
aria-label="Dark Mode"
className={`rounded-full p-1 transition-all ${
selectedTheme === "dark"
? "bg-blue-500 text-white"
: "bg-transparent"
? "bg-primary text-primary-foreground"
: "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>
</ToggleGroup>
);

View File

@@ -149,7 +149,7 @@ export default function TocObserver({
return (
<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">
{data.map(({ href, level, text }, index) => {
const id = href.slice(1);
@@ -172,7 +172,7 @@ export default function TocObserver({
{/* Vertical line */}
<div className={clsx(
"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 && (
<motion.div
@@ -187,11 +187,11 @@ export default function TocObserver({
{/* Horizontal line */}
<div className={clsx(
"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 && (
<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 }}
animate={{ scaleX: scrollProgress }}
transition={{ duration: 0.3, delay: 0.1 }}
@@ -207,8 +207,8 @@ export default function TocObserver({
className={clsx(
"relative flex items-center py-2 transition-colors",
{
"text-primary dark:text-primary-400 font-medium": isActive,
"text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-stone-200": !isActive,
"text-primary dark:text-primary font-medium": isActive,
"text-muted-foreground hover:text-foreground dark:hover:text-foreground/90": !isActive,
}
)}
style={{
@@ -229,13 +229,13 @@ export default function TocObserver({
<div className={clsx(
"w-1.5 h-1.5 rounded-full transition-all duration-300 relative z-10",
{
"bg-primary scale-100": isActive,
"bg-stone-300 dark:bg-stone-600 scale-75 group-hover:scale-100 group-hover:bg-primary/50": !isActive,
"bg-primary scale-100 dark:bg-primary/90": 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 && (
<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 }}
animate={{ scale: 1.8 }}
transition={{

View File

@@ -30,6 +30,11 @@
"title": "Getting Started",
"href": "/getting-started",
"noLink": true,
"context": {
"icon": "MousePointerClick",
"description": "Tutorial untuk memulai",
"title": "Panduan"
},
"items": [
{ "title": "Introduction", "href": "/introduction" },
{ "title": "Installasi Plugin", "href": "/installation" },
@@ -41,6 +46,11 @@
"title": "Addon Sejoli Pro",
"href": "/plugins",
"noLink": true,
"context": {
"icon": "Package",
"description": "Panduan untuk plugin addons",
"title": "Plugins"
},
"items": [
{
"title": "Member Area UI",

View File

@@ -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;
}

View File

@@ -26,6 +26,7 @@ import Accordion from "@/components/markdown/AccordionMdx";
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";
// add custom components
const components = {
@@ -47,8 +48,13 @@ const components = {
Accordion,
CardGroup,
Kbd,
// Release Note Components
Release,
Changes,
// File Tree Components
File,
Files,
Folder,
};
// 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;
}
}
*/

View File

@@ -1,9 +1,16 @@
import docuConfig from "@/docu.json"; // Import JSON file
export type ContextInfo = {
icon: string;
description: string;
title?: string;
};
export type EachRoute = {
title: string;
href: string;
noLink?: boolean; // Sekarang mendukung boolean
noLink?: boolean;
context?: ContextInfo;
items?: EachRoute[];
};

8628
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "docubook",
"version": "1.11.0",
"version": "1.13.6",
"private": true,
"scripts": {
"dev": "next dev",
@@ -55,5 +55,5 @@
"tailwindcss": "^3.4.10",
"typescript": "^5"
},
"packageManager": "npm@11.3.0"
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -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;
}

View File

@@ -3,7 +3,6 @@
@tailwind utilities;
@import url("../styles/syntax.css");
@import url("../styles/editor.css");
/* ocean */
@layer base {

View File

@@ -9,7 +9,6 @@ const config = {
"./src/**/*.{ts,tsx}",
],
prefix: "",
safelist: ["line-clamp-3","line-clam-2"],
theme: {
container: {
center: true,