Merge pull request #6 from DocuBook/dev-4eb3d42
chore: Sync package version v1.14.2 The original Search component was monolithic, handling state management, UI triggers, and modal content logic in a single file. This made it difficult to maintain and extend. This commit refactors the component into three distinct, more focused components: - `Search.tsx`: The new parent component that manages the dialog's open/close state and will serve as the entry point for selecting different search types (e.g., default, Algolia). - `SearchTrigger.tsx`: A simple component responsible only for rendering the search button/input UI that opens the modal. - `SearchModal.tsx`: Encapsulates all logic and UI for the search dialog itself, including the input field, results display, and keyboard navigation. This separation of concerns improves readability, maintainability, and prepares the codebase for future scalability.
This commit is contained in:
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
|
||||
import AnimatedShinyText from "@/components/ui/animated-shiny-text";
|
||||
import { getMetadata } from "@/app/layout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
// v1.14.1
|
||||
|
||||
export const metadata = getMetadata({
|
||||
title: "Home",
|
||||
});
|
||||
@@ -25,7 +25,7 @@ export default function Home() {
|
||||
)}
|
||||
>
|
||||
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-100 hover:duration-300 hover:dark:text-neutral-200">
|
||||
<span>🚀 New Version - Release v1.14.1</span>
|
||||
<span>🚀 New Version - Release v1.14.2</span>
|
||||
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||
</AnimatedShinyText>
|
||||
</div>
|
||||
|
||||
198
components/SearchModal.tsx
Normal file
198
components/SearchModal.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
import { ArrowUpIcon, ArrowDownIcon, CornerDownLeftIcon, FileTextIcon } from "lucide-react";
|
||||
import Anchor from "./anchor";
|
||||
import { advanceSearch, cn } from "@/lib/utils";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { page_routes } from "@/lib/routes-config";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
type ContextInfo = {
|
||||
icon: string;
|
||||
description: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
type SearchResult = {
|
||||
title: string;
|
||||
href: string;
|
||||
noLink?: boolean;
|
||||
items?: undefined;
|
||||
score?: number;
|
||||
context?: ContextInfo;
|
||||
};
|
||||
|
||||
const paddingMap = {
|
||||
1: "pl-2",
|
||||
2: "pl-4",
|
||||
3: "pl-10",
|
||||
} as const;
|
||||
|
||||
interface SearchModalProps {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
||||
const router = useRouter();
|
||||
const [searchedInput, setSearchedInput] = useState("");
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSearchedInput("");
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const filteredResults = useMemo<SearchResult[]>(() => {
|
||||
const trimmedInput = searchedInput.trim();
|
||||
|
||||
if (trimmedInput.length < 3) {
|
||||
return page_routes
|
||||
.filter((route) => !route.href.endsWith('/'))
|
||||
.slice(0, 6)
|
||||
.map((route: { title: string; href: string; noLink?: boolean; context?: ContextInfo }) => ({
|
||||
title: route.title,
|
||||
href: route.href,
|
||||
noLink: route.noLink,
|
||||
context: route.context,
|
||||
}));
|
||||
}
|
||||
return advanceSearch(trimmedInput) as unknown as SearchResult[];
|
||||
}, [searchedInput]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [filteredResults]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleNavigation = (event: KeyboardEvent) => {
|
||||
if (!isOpen || filteredResults.length === 0) return;
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
setSelectedIndex((prev) => (prev + 1) % filteredResults.length);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length);
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const selectedItem = filteredResults[selectedIndex];
|
||||
if (selectedItem) {
|
||||
router.push(`/docs${selectedItem.href}`);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleNavigation);
|
||||
return () => window.removeEventListener("keydown", handleNavigation);
|
||||
}, [isOpen, filteredResults, selectedIndex, router, setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemRefs.current[selectedIndex]) {
|
||||
itemRefs.current[selectedIndex]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
return (
|
||||
<DialogContent className="p-0 max-w-[650px] sm:top-[38%] top-[45%] !rounded-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="sr-only">Search Documentation</DialogTitle>
|
||||
<DialogDescription className="sr-only">Search through the documentation</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<input
|
||||
value={searchedInput}
|
||||
onChange={(e) => setSearchedInput(e.target.value)}
|
||||
placeholder="Type something to search..."
|
||||
autoFocus
|
||||
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full"
|
||||
aria-label="Search documentation"
|
||||
/>
|
||||
|
||||
{filteredResults.length == 0 && searchedInput && (
|
||||
<p className="text-muted-foreground mx-auto mt-2 text-sm">
|
||||
No results found for{" "}
|
||||
<span className="text-primary">{`"${searchedInput}"`}</span>
|
||||
</p>
|
||||
)}
|
||||
<ScrollArea className="max-h-[400px] overflow-y-auto">
|
||||
<div className="flex flex-col items-start overflow-y-auto sm:px-2 px-1 pb-4">
|
||||
{filteredResults.map((item, index) => {
|
||||
const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap;
|
||||
const paddingClass = paddingMap[level] || 'pl-2';
|
||||
const isActive = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<DialogClose key={item.href} asChild>
|
||||
<Anchor
|
||||
ref={(el) => {
|
||||
itemRefs.current[index] = el as HTMLDivElement | null;
|
||||
}}
|
||||
className={cn(
|
||||
"dark:hover:bg-accent/15 hover:bg-accent/10 w-full px-3 rounded-sm text-sm flex items-center gap-2.5",
|
||||
isActive && "bg-primary/20 dark:bg-primary/30",
|
||||
paddingClass
|
||||
)}
|
||||
href={`/docs${item.href}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center w-full h-full py-3 gap-1.5 px-2 justify-between",
|
||||
level > 1 && "border-l pl-4"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<FileTextIcon className="h-[1.1rem] w-[1.1rem] mr-1" />
|
||||
<span>{item.title}</span>
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className="hidden md:flex items-center text-xs text-muted-foreground">
|
||||
<span>Return</span>
|
||||
<CornerDownLeftIcon className="h-3 w-3 ml-1" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Anchor>
|
||||
</DialogClose>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<DialogFooter className="md:flex md:justify-start hidden h-14 px-6 bg-transparent border-t text-[14px] outline-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
||||
<ArrowUpIcon className="w-3 h-3"/>
|
||||
</span>
|
||||
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
||||
<ArrowDownIcon className="w-3 h-3"/>
|
||||
</span>
|
||||
<p className="text-muted-foreground">to navigate</p>
|
||||
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
||||
<CornerDownLeftIcon className="w-3 h-3"/>
|
||||
</span>
|
||||
<p className="text-muted-foreground">to select</p>
|
||||
<span className="dark:bg-accent/15 bg-slate-200 border rounded px-2 py-1">
|
||||
esc
|
||||
</span>
|
||||
<p className="text-muted-foreground">to close</p>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
31
components/SearchTrigger.tsx
Normal file
31
components/SearchTrigger.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { CommandIcon, SearchIcon } from "lucide-react";
|
||||
import { DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export function SearchTrigger() {
|
||||
return (
|
||||
<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-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-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"
|
||||
readOnly // This input is for display only
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
import { ArrowUpIcon, ArrowDownIcon, CommandIcon, FileTextIcon, SearchIcon, CornerDownLeftIcon } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import Anchor from "./anchor";
|
||||
import { advanceSearch, cn } from "@/lib/utils";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { page_routes } from "@/lib/routes-config";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { SearchTrigger } from "@/components/SearchTrigger";
|
||||
import { SearchModal } from "@/components/SearchModal";
|
||||
|
||||
// Define the ContextInfo type to match the one in routes-config
|
||||
type ContextInfo = {
|
||||
icon: string;
|
||||
description: string;
|
||||
title?: string;
|
||||
};
|
||||
// Define props for the Search component
|
||||
interface SearchProps {
|
||||
/**
|
||||
* Specify the type of search engine to use.
|
||||
* @default 'default'
|
||||
*/
|
||||
type?: "default" | "algolia";
|
||||
}
|
||||
|
||||
type SearchResult = {
|
||||
title: string;
|
||||
href: string;
|
||||
noLink?: boolean;
|
||||
items?: undefined;
|
||||
score?: number;
|
||||
context?: ContextInfo;
|
||||
};
|
||||
|
||||
export default function Search() {
|
||||
const router = useRouter();
|
||||
const [searchedInput, setSearchedInput] = useState("");
|
||||
export default function Search({ type = "default" }: SearchProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
// Effect to handle keyboard shortcut (Cmd/Ctrl + K)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
||||
event.preventDefault();
|
||||
setIsOpen(true);
|
||||
setIsOpen((open) => !open);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,191 +32,20 @@ export default function Search() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
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);
|
||||
}, [filteredResults]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleNavigation = (event: KeyboardEvent) => {
|
||||
if (!isOpen || filteredResults.length === 0) return;
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
setSelectedIndex((prev) => (prev + 1) % filteredResults.length);
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length);
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const selectedItem = filteredResults[selectedIndex];
|
||||
if (selectedItem) {
|
||||
router.push(`/docs${selectedItem.href}`);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleNavigation);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleNavigation);
|
||||
};
|
||||
}, [isOpen, filteredResults, selectedIndex, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemRefs.current[selectedIndex]) {
|
||||
itemRefs.current[selectedIndex]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
// Here you can add logic for different search types if needed in the future
|
||||
if (type === "algolia") {
|
||||
// return <AlgoliaSearchComponent />; // Example for future implementation
|
||||
console.warn("Tipe pencarian 'algolia' belum diimplementasikan.");
|
||||
// For now, we will fall back to the default search implementation
|
||||
}
|
||||
|
||||
// Render the default search components
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSearchedInput("");
|
||||
setIsOpen(open);
|
||||
}}
|
||||
>
|
||||
<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-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-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"
|
||||
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>
|
||||
<DialogContent className="p-0 max-w-[650px] sm:top-[38%] top-[45%] !rounded-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="sr-only">Search Documentation</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription className="sr-only">
|
||||
Search through the documentation
|
||||
</DialogDescription>
|
||||
<input
|
||||
value={searchedInput}
|
||||
onChange={(e) => setSearchedInput(e.target.value)}
|
||||
placeholder="Type something to search..."
|
||||
autoFocus
|
||||
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full"
|
||||
aria-label="Search documentation"
|
||||
/>
|
||||
{filteredResults.length == 0 && searchedInput && (
|
||||
<p className="text-muted-foreground mx-auto mt-2 text-sm">
|
||||
No results found for{" "}
|
||||
<span className="text-primary">{`"${searchedInput}"`}</span>
|
||||
</p>
|
||||
)}
|
||||
<ScrollArea className="max-h-[400px] overflow-y-auto">
|
||||
<div className="flex flex-col items-start overflow-y-auto sm:px-2 px-1 pb-4">
|
||||
{filteredResults.map((item, index) => {
|
||||
const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap;
|
||||
const paddingClass = paddingMap[level];
|
||||
const isActive = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<DialogClose key={item.href} asChild>
|
||||
<Anchor
|
||||
ref={(el) => {
|
||||
itemRefs.current[index] = el as HTMLDivElement | null;
|
||||
}}
|
||||
className={cn(
|
||||
"dark:hover:bg-accent/15 hover:bg-accent/10 w-full px-3 rounded-sm text-sm flex items-center gap-2.5",
|
||||
isActive && "bg-primary/20 dark:bg-primary/30",
|
||||
paddingClass
|
||||
)}
|
||||
href={`/docs${item.href}`}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center w-full h-full py-3 gap-1.5 px-2 justify-between",
|
||||
level > 1 && "border-l pl-4"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<FileTextIcon className="h-[1.1rem] w-[1.1rem] mr-1" />
|
||||
<span>{item.title}</span>
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className="hidden md:flex items-center text-xs text-muted-foreground">
|
||||
<span>Return</span>
|
||||
<CornerDownLeftIcon className="h-3 w-3 ml-1" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Anchor>
|
||||
</DialogClose>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<DialogFooter className="md:flex md:justify-start hidden h-14 px-6 bg-transparent border-t text-[14px] outline-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
||||
<ArrowUpIcon className="w-3 h-3"/>
|
||||
</span>
|
||||
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
||||
<ArrowDownIcon className="w-3 h-3"/>
|
||||
</span>
|
||||
<p className="text-muted-foreground">to navigate</p>
|
||||
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
||||
<CornerDownLeftIcon className="w-3 h-3"/>
|
||||
</span>
|
||||
<p className="text-muted-foreground">to select</p>
|
||||
<span className="dark:bg-accent/15 bg-slate-200 border rounded px-2 py-1">
|
||||
esc
|
||||
</span>
|
||||
<p className="text-muted-foreground">to close</p>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<SearchTrigger />
|
||||
<SearchModal isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const paddingMap = {
|
||||
1: "pl-2",
|
||||
2: "pl-4",
|
||||
3: "pl-10",
|
||||
} as const;
|
||||
|
||||
@@ -8,6 +8,22 @@ date: 02-08-2025
|
||||
This changelog contains a list of all the changes made to the DocuBook template. It will be updated with each new release and will include information about new features, bug fixes, and other improvements.
|
||||
</Note>
|
||||
|
||||
<div className="sr-only">
|
||||
### v 1.14.2
|
||||
</div>
|
||||
|
||||
<Release version="1.14.2" date="2025-08-05" title="Refactor & Fix: Decouple Search Component and Resolve Type Errors">
|
||||
<Changes type="added">
|
||||
- Refactor Search component into three distinct components: Search, SearchTrigger, and SearchModal for better maintainability and scalability.
|
||||
- New SearchTrigger components
|
||||
- New SearchModal components
|
||||
</Changes>
|
||||
<Changes type="fixed">
|
||||
- Resolve TypeScript error for missing 'noLink' property on the 'Page' type after refactoring.
|
||||
- Fix TypeScript error for missing 'context' property by providing a complete inline type annotation in the SearchModal component.
|
||||
</Changes>
|
||||
</Release>
|
||||
|
||||
<div className="sr-only">
|
||||
### v 1.14.0
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docubook",
|
||||
"version": "1.14.1",
|
||||
"version": "1.14.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user