diff --git a/app/layout.tsx b/app/layout.tsx index b5b00db..26b6794 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import '../styles/globals.css'; import type { Metadata } from 'next'; import { siteConfig } from '@/lib/config'; +import { getAllPostsSorted } from '@/lib/posts'; import { LayoutShell } from '@/components/layout-shell'; import { ThemeProvider } from 'next-themes'; import { Playfair_Display, LXGW_WenKai_TC } from 'next/font/google'; @@ -55,12 +56,15 @@ export const metadata: Metadata = { } }; -export default function RootLayout({ +export default async function RootLayout({ children }: { children: React.ReactNode; }) { const theme = siteConfig.theme; + const recentPosts = getAllPostsSorted() + .slice(0, 5) + .map((p) => ({ title: p.title, url: p.url })); // WebSite Schema const websiteSchema = { @@ -131,7 +135,7 @@ export default function RootLayout({ }} /> - {children} + {children} diff --git a/components/layout-shell.tsx b/components/layout-shell.tsx index cee4b68..27dd591 100644 --- a/components/layout-shell.tsx +++ b/components/layout-shell.tsx @@ -9,10 +9,15 @@ const BackToTop = dynamic(() => import('./back-to-top').then(mod => ({ default: ssr: false, }); -export function LayoutShell({ children }: { children: React.ReactNode }) { +interface LayoutShellProps { + children: React.ReactNode; + recentPosts?: { title: string; url: string }[]; +} + +export function LayoutShell({ children, recentPosts = [] }: LayoutShellProps) { return (
- +
{children}
diff --git a/components/post-layout.tsx b/components/post-layout.tsx index c134e7c..da30429 100644 --- a/components/post-layout.tsx +++ b/components/post-layout.tsx @@ -4,18 +4,13 @@ import { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { FiList, FiX } from 'react-icons/fi'; import dynamic from 'next/dynamic'; -import { clsx, type ClassValue } from 'clsx'; -import { twMerge } from 'tailwind-merge'; +import { cn } from '@/lib/utils'; // Lazy load PostToc since it's not critical for initial render const PostToc = dynamic(() => import('./post-toc').then(mod => ({ default: mod.PostToc })), { ssr: false, }); -function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - export function PostLayout({ children, hasToc = true, contentKey }: { children: React.ReactNode; hasToc?: boolean; contentKey?: string }) { const [isTocOpen, setIsTocOpen] = useState(false); // Default closed on mobile const [isDesktopTocOpen, setIsDesktopTocOpen] = useState(hasToc); // Separate state for desktop diff --git a/components/post-list-with-controls.tsx b/components/post-list-with-controls.tsx index 5f6cd0a..a44d7ee 100644 --- a/components/post-list-with-controls.tsx +++ b/components/post-list-with-controls.tsx @@ -113,10 +113,10 @@ export function PostListWithControls({ posts, pageSize }: Props) { setSearchTerm(event.target.value)} - className="w-full rounded-full border border-slate-200 bg-white py-1.5 pl-9 pr-3 text-sm text-slate-700 shadow-sm transition duration-180 ease-snappy focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:focus:ring-blue-500" + className="w-full rounded-full border border-slate-200 bg-white py-1.5 pl-9 pr-3 text-sm text-slate-700 shadow-sm transition duration-180 ease-snappy focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:focus:border-accent dark:focus:ring-accent/30" />
diff --git a/components/search-modal.tsx b/components/search-modal.tsx index e6e779e..a4176d0 100644 --- a/components/search-modal.tsx +++ b/components/search-modal.tsx @@ -1,78 +1,69 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { FiSearch, FiX } from 'react-icons/fi'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Command } from 'cmdk'; +import { + FiSearch, + FiHome, + FiFileText, + FiTag, + FiBook +} from 'react-icons/fi'; +import { cn } from '@/lib/utils'; + +interface PagefindResult { + url: string; + meta: { title?: string }; + excerpt?: string; +} + +interface QuickAction { + id: string; + title: string; + url: string; + icon: React.ReactNode; +} interface SearchModalProps { isOpen: boolean; onClose: () => void; + recentPosts?: { title: string; url: string }[]; } -export function SearchModal({ isOpen, onClose }: SearchModalProps) { - const [isLoaded, setIsLoaded] = useState(false); - const searchContainerRef = useRef(null); - const pagefindUIRef = useRef(null); +export function SearchModal({ + isOpen, + onClose, + recentPosts = [] +}: SearchModalProps) { + const router = useRouter(); + const [search, setSearch] = useState(''); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [pagefindReady, setPagefindReady] = useState(false); + const pagefindRef = useRef<{ + init: () => void; + options: (opts: { bundlePath: string }) => Promise; + preload: (query: string) => void; + debouncedSearch: ( + query: string, + opts: object, + debounceMs: number + ) => Promise<{ results: { data: () => Promise }[] } | null>; + } | null>(null); + // Initialize Pagefind when modal opens useEffect(() => { if (!isOpen) return; - let link: HTMLLinkElement | null = null; - let script: HTMLScriptElement | null = null; - - // Load Pagefind UI dynamically when modal opens const loadPagefind = async () => { - if (pagefindUIRef.current) { - // Already loaded - return; - } - try { - // Load Pagefind UI CSS - link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = '/_pagefind/pagefind-ui.css'; - document.head.appendChild(link); - - // Load Pagefind UI JS - script = document.createElement('script'); - script.src = '/_pagefind/pagefind-ui.js'; - script.onload = () => { - if (searchContainerRef.current && (window as any).PagefindUI) { - pagefindUIRef.current = new (window as any).PagefindUI({ - element: searchContainerRef.current, - bundlePath: '/_pagefind/', - showSubResults: true, - showImages: false, - excerptLength: 15, - resetStyles: false, - autofocus: true, - translations: { - placeholder: '搜尋文章...', - clear_search: '清除', - load_more: '載入更多結果', - search_label: '搜尋此網站', - filters_label: '篩選', - zero_results: '找不到 [SEARCH_TERM] 的結果', - many_results: '找到 [COUNT] 個 [SEARCH_TERM] 的結果', - one_result: '找到 [COUNT] 個 [SEARCH_TERM] 的結果', - alt_search: '找不到 [SEARCH_TERM] 的結果。改為顯示 [DIFFERENT_TERM] 的結果', - search_suggestion: '找不到 [SEARCH_TERM] 的結果。請嘗試以下搜尋:', - searching: '搜尋中...' - } - }); - setIsLoaded(true); - - // Auto-focus the search input after a short delay - setTimeout(() => { - const input = searchContainerRef.current?.querySelector('input[type="search"]') as HTMLInputElement; - if (input) { - input.focus(); - } - }, 100); - } - }; - document.head.appendChild(script); + const pagefindUrl = `${window.location.origin}/_pagefind/pagefind.js`; + const pagefind = await import(/* webpackIgnore: true */ pagefindUrl); + await pagefind.options({ bundlePath: '/_pagefind/' }); + pagefind.init(); + pagefindRef.current = pagefind; + setPagefindReady(true); } catch (error) { console.error('Failed to load Pagefind:', error); } @@ -80,102 +71,179 @@ export function SearchModal({ isOpen, onClose }: SearchModalProps) { loadPagefind(); - // Cleanup function to prevent duplicate initializations return () => { - if (link && link.parentNode) { - link.parentNode.removeChild(link); - } - if (script && script.parentNode) { - script.parentNode.removeChild(script); - } - if (pagefindUIRef.current && pagefindUIRef.current.destroy) { - pagefindUIRef.current.destroy(); - pagefindUIRef.current = null; - } + pagefindRef.current = null; + setPagefindReady(false); + setSearch(''); + setResults([]); }; }, [isOpen]); + // Debounced search when user types useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape' && isOpen) { - onClose(); - } - }; - - document.addEventListener('keydown', handleEscape); - return () => document.removeEventListener('keydown', handleEscape); - }, [isOpen, onClose]); - - useEffect(() => { - // Prevent body scroll when modal is open - if (isOpen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; + const query = search.trim(); + if (!query || !pagefindRef.current) { + setResults([]); + setLoading(false); + return; } - return () => { - document.body.style.overflow = ''; - }; - }, [isOpen]); - if (!isOpen) return null; + setLoading(true); + pagefindRef.current.preload(query); - // Use portal to render modal at document body level to avoid z-index stacking context issues - if (typeof window === 'undefined') return null; + const timer = setTimeout(async () => { + const pagefind = pagefindRef.current; + if (!pagefind) return; - return createPortal( -
r.data()); + const items = await Promise.all(dataPromises); + setResults(items); + setLoading(false); + }, 300); + + return () => clearTimeout(timer); + }, [search, pagefindReady]); + + const handleSelect = useCallback( + (url: string) => { + onClose(); + router.push(url); + }, + [onClose, router] + ); + + const navActions: QuickAction[] = [ + { id: 'home', title: '首頁', url: '/', icon: }, + { + id: 'blog', + title: '部落格', + url: '/blog', + icon: + }, + { + id: 'tags', + title: '標籤', + url: '/tags', + icon: + } + ]; + + const recentPostActions: QuickAction[] = recentPosts.map((p) => ({ + id: `post-${p.url}`, + title: p.title, + url: p.url, + icon: + })); + + return ( + !open && onClose()} + label="全站搜尋" + shouldFilter={false} + className="fixed left-1/2 top-[20%] z-[9999] w-full max-w-2xl -translate-x-1/2 rounded-2xl border border-white/40 bg-white/95 shadow-2xl backdrop-blur-md dark:border-white/10 dark:bg-slate-900/95" > -
e.stopPropagation()} - > - {/* Header */} -
-
- - 全站搜尋 -
- -
- - {/* Search Container */} -
-
- {!isLoaded && ( -
-
-
-

- 載入搜尋引擎... -

-
-
- )} -
- - {/* Footer */} -
-
- 按 ESC 關閉 - 支援中英文全文搜尋 -
-
+
+ +
-
, - document.body + + + {loading && ( + + 搜尋中… + + )} + + {!loading && !search.trim() && ( + <> + + {navActions.map((action) => ( + handleSelect(action.url)} + className={cn( + 'flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-700 outline-none transition-colors', + 'data-[selected=true]:bg-slate-100 data-[selected=true]:text-slate-900', + 'dark:text-slate-300 dark:data-[selected=true]:bg-slate-800 dark:data-[selected=true]:text-slate-100' + )} + > + + {action.icon} + + {action.title} + + ))} + + {recentPostActions.length > 0 && ( + + {recentPostActions.map((action) => ( + handleSelect(action.url)} + className={cn( + 'flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-slate-700 outline-none transition-colors', + 'data-[selected=true]:bg-slate-100 data-[selected=true]:text-slate-900', + 'dark:text-slate-300 dark:data-[selected=true]:bg-slate-800 dark:data-[selected=true]:text-slate-100' + )} + > + + {action.icon} + + {action.title} + + ))} + + )} + + )} + + {!loading && search.trim() && results.length > 0 && ( + + {results.map((result, i) => ( + handleSelect(result.url)} + className={cn( + 'flex cursor-pointer flex-col gap-0.5 rounded-lg px-3 py-2.5 outline-none transition-colors', + 'data-[selected=true]:bg-slate-100 dark:data-[selected=true]:bg-slate-800' + )} + > + + {result.meta?.title ?? result.url} + + {result.excerpt && ( + + )} + + ))} + + )} + + + 找不到結果 + + + +
+ ESC 關閉 + ⌘K 開啟 +
+ ); } diff --git a/components/site-header.tsx b/components/site-header.tsx index eabd14d..0398928 100644 --- a/components/site-header.tsx +++ b/components/site-header.tsx @@ -15,7 +15,11 @@ const SearchModal = dynamic( { ssr: false } ); -export function SiteHeader() { +interface SiteHeaderProps { + recentPosts?: { title: string; url: string }[]; +} + +export function SiteHeader({ recentPosts = [] }: SiteHeaderProps) { const [isSearchOpen, setIsSearchOpen] = useState(false); const pages = allPages .slice() @@ -93,6 +97,7 @@ export function SiteHeader() { setIsSearchOpen(false)} + recentPosts={recentPosts} />
diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..2819a83 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/package-lock.json b/package-lock.json index 6a3220a..a82fd15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "license": "ISC", "dependencies": { "@emotion/is-prop-valid": "^1.4.0", + "@radix-ui/react-dialog": "^1.1.15", "@vercel/og": "^0.8.5", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "contentlayer2": "^0.5.8", "gray-matter": "^4.0.3", "markdown-wasm": "^1.2.0", @@ -2852,6 +2854,337 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@resvg/resvg-wasm": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", @@ -3336,7 +3669,7 @@ "version": "19.2.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3346,7 +3679,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -3990,6 +4323,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -4585,6 +4930,22 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/collapse-white-space": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", @@ -4776,7 +5137,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4932,6 +5293,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -6150,6 +6517,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -9754,6 +10130,75 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11469,6 +11914,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 0df43ba..b6f883f 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "type": "module", "dependencies": { "@emotion/is-prop-valid": "^1.4.0", + "@radix-ui/react-dialog": "^1.1.15", "@vercel/og": "^0.8.5", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "contentlayer2": "^0.5.8", "gray-matter": "^4.0.3", "markdown-wasm": "^1.2.0", diff --git a/styles/globals.css b/styles/globals.css index 90af87f..055fef1 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -493,61 +493,9 @@ body { } } -/* Pagefind Search Styles - Use CSS variables to override defaults */ -:root { - --pagefind-ui-scale: 1; - --pagefind-ui-primary: #2563eb; - --pagefind-ui-text: #0f172a; - --pagefind-ui-background: #ffffff; - --pagefind-ui-border: #e2e8f0; - --pagefind-ui-tag: #f1f5f9; - --pagefind-ui-border-width: 1px; - --pagefind-ui-border-radius: 0.5rem; - --pagefind-ui-font: var(--font-system-sans); -} - -.dark { - --pagefind-ui-primary: #60a5fa; - --pagefind-ui-text: #f1f5f9; - --pagefind-ui-background: #0f172a; - --pagefind-ui-border: #475569; - --pagefind-ui-tag: #334155; -} - -/* Enhanced text colors for better readability */ -.pagefind-ui__result-title { - color: var(--pagefind-ui-text) !important; -} - -.dark .pagefind-ui__result-title { - color: #f8fafc !important; -} - -.pagefind-ui__result-excerpt { - color: #475569 !important; -} - -.dark .pagefind-ui__result-excerpt { - color: #cbd5e1 !important; -} - -.pagefind-ui__result-link { - color: var(--pagefind-ui-primary) !important; -} - -.dark .pagefind-ui__result-link { - color: #93c5fd !important; -} - -/* Additional custom styling for highlights */ -.pagefind-ui__result-excerpt mark { - @apply bg-yellow-200 font-semibold text-slate-900 dark:bg-yellow-600 dark:text-slate-100; - padding: 0.125rem 0.25rem; - border-radius: 0.25rem; -} - -.pagefind-ui__search-input:focus { - @apply ring-2 ring-blue-500 dark:ring-blue-400; +/* Search modal overlay (cmdk / Radix Dialog) */ +[data-radix-dialog-overlay] { + @apply bg-black/50 backdrop-blur-sm; } /* Code Syntax Highlighting Styles (rehype-pretty-code) */