feat(ui): update search component and improve responsive design

- Show only search icon on mobile (< 768px) in navbar
- Refactor Anchor component with better type safety and link handling
- Adjust spacing in Table of Contents and main content
- Improve responsive layout and consistency
This commit is contained in:
Wildan Nursahidan
2025-05-26 11:06:58 +07:00
parent 49d59e0476
commit bd400695ff
6 changed files with 76 additions and 28 deletions

View File

@@ -77,7 +77,7 @@ export default async function DocsPage({ params: { slug = [] } }: PageProps) {
return (
<div className="flex items-start gap-10">
<div className="flex-[4.5] pt-4 lg:pt-10">
<div className="flex-[4.5] pt-5">
<MobToc tocs={tocs} />
<DocsBreadcrumb paths={slug} />
<Typography>

View File

@@ -8,7 +8,7 @@ export default function DocsLayout({
return (
<div className="flex items-start gap-8">
<Leftbar key="leftbar" />
<div className="flex-[5.25] py-4 px-1">
<div className="flex-[5.25] px-1">
{children}
</div>
</div>

View File

@@ -1,37 +1,78 @@
"use client";
import { cn } from "@/lib/utils";
import Link from "next/link";
import Link, { LinkProps } from "next/link";
import { usePathname } from "next/navigation";
import { ComponentProps, forwardRef } from "react";
import { forwardRef } from "react";
type AnchorProps = ComponentProps<typeof Link> & {
type AnchorProps = LinkProps & {
absolute?: boolean;
activeClassName?: string;
disabled?: boolean;
};
className?: string;
children: React.ReactNode;
} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps>;
const Anchor = forwardRef<HTMLAnchorElement, AnchorProps>(
({ absolute, className = "", activeClassName = "", disabled, children, ...props }, ref) => {
({
absolute = false,
className = "",
activeClassName = "",
disabled = false,
children,
href,
...props
}, ref) => {
const path = usePathname();
const href = props.href.toString();
const hrefStr = href?.toString() || '';
// Deteksi URL eksternal menggunakan regex
const isExternal = /^(https?:\/\/|\/\/)/.test(href);
// Check if URL is external
const isExternal = /^(https?:\/\/|\/\/)/.test(hrefStr);
let isMatch = absolute
? href.split("/")[1] === path.split("/")[1]
: path === href;
// Check if current path matches the link
const isActive = absolute
? hrefStr.split("/")[1] === path?.split("/")[1]
: path === hrefStr;
if (isExternal) isMatch = false; // Hindari mencocokkan URL eksternal
if (disabled)
return (
<div className={cn(className, "cursor-not-allowed")}>{children}</div>
// Apply active class only for internal links
const linkClassName = cn(
'transition-colors hover:text-primary',
className,
!isExternal && isActive && activeClassName
);
if (disabled) {
return (
<Link ref={ref} className={cn(className, isMatch && activeClassName)} {...props}>
<span className={cn(linkClassName, "cursor-not-allowed opacity-50")}>
{children}
</span>
);
}
if (isExternal) {
return (
<a
ref={ref}
href={hrefStr}
className={linkClassName}
target="_blank"
rel="noopener noreferrer"
{...props}
>
{children}
</a>
);
}
return (
<Link
ref={ref}
href={hrefStr}
className={linkClassName}
{...props}
>
{children}
</Link>
);

View File

@@ -87,7 +87,7 @@ export default function MobToc({ tocs }: MobTocProps) {
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<div className="w-full bg-background/95 backdrop-blur-sm border-b border-stone-200 dark:border-stone-800 shadow-sm">
<div className="md:px-8 px-4 py-2">
<div className="sm:px-8 px-4 py-2">
<Button
variant="ghost"
size="sm"

View File

@@ -98,9 +98,14 @@ export default function Search() {
>
<DialogTrigger asChild>
<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" />
</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" />
<Input
className="md: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"
type="search"
/>
@@ -109,6 +114,8 @@ export default function Search() {
<span>K</span>
</div>
</div>
</div>
</div>
</DialogTrigger>
<DialogContent className="p-0 max-w-[650px] sm:top-[38%] top-[45%] !rounded-md">
<DialogHeader>

View File

@@ -9,7 +9,7 @@ export default async function Toc({ path }: { path: string }) {
const tocs = await getDocsTocs(path);
return (
<div className="lg:flex hidden toc flex-[1.5] min-w-[238px] py-9 sticky top-16 h-[calc(100vh-4rem)]">
<div className="lg:flex hidden toc flex-[1.5] min-w-[238px] py-5 sticky top-16 h-[calc(100vh-4rem)]">
<div className="flex flex-col h-full w-full px-2 gap-2 mb-auto">
<div className="flex items-center gap-2">
<ListIcon className="w-4 h-4" />