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:
@@ -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
|
||||
// Apply active class only for internal links
|
||||
const linkClassName = cn(
|
||||
'transition-colors hover:text-primary',
|
||||
className,
|
||||
!isExternal && isActive && activeClassName
|
||||
);
|
||||
|
||||
if (disabled)
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className={cn(className, "cursor-not-allowed")}>{children}</div>
|
||||
<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} className={cn(className, isMatch && activeClassName)} {...props}>
|
||||
<Link
|
||||
ref={ref}
|
||||
href={hrefStr}
|
||||
className={linkClassName}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -98,15 +98,22 @@ export default function Search() {
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<div className="relative flex-1 cursor-pointer max-w-[140px]">
|
||||
<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"
|
||||
placeholder="Search"
|
||||
type="search"
|
||||
/>
|
||||
<div className="flex absolute top-1/2 -translate-y-1/2 right-2 text-xs font-medium font-mono items-center gap-0.5 dark:bg-accent bg-accent text-white px-2 py-0.5 rounded-full">
|
||||
<CommandIcon className="w-3 h-3" />
|
||||
<span>K</span>
|
||||
<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="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"
|
||||
/>
|
||||
<div className="flex absolute top-1/2 -translate-y-1/2 right-2 text-xs font-medium font-mono items-center gap-0.5 dark:bg-accent bg-accent text-white px-2 py-0.5 rounded-full">
|
||||
<CommandIcon className="w-3 h-3" />
|
||||
<span>K</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user