add floating navbar

This commit is contained in:
vergiLgood1 2025-02-21 19:27:54 +07:00
parent 7de1201675
commit 3ba462d797
3 changed files with 257 additions and 193 deletions

View File

@ -23,7 +23,7 @@ import {
import { MoreHorizontal } from "lucide-react"; import { MoreHorizontal } from "lucide-react";
import { InboxDrawer } from "@/components/inbox-drawer"; import { InboxDrawer } from "@/components/inbox-drawer";
import { ThemeSwitcher } from "@/components/theme-switcher"; import { ThemeSwitcher } from "@/components/theme-switcher";
import ActionSearchBar from "@/components/action-search-bar"; import FloatingActionSearchBar from "@/components/floating-action-search-bar";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
export default async function Layout({ export default async function Layout({
@ -37,29 +37,22 @@ export default async function Layout({
<SidebarInset> <SidebarInset>
{/* Navigation bar with SidebarTrigger and Breadcrumbs */} {/* Navigation bar with SidebarTrigger and Breadcrumbs */}
<nav className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <nav className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-12 items-center px-4"> <div className="flex h-16 shrink-0 items-center justify-end border-b px-4 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 flex-1">
<SidebarTrigger className="" /> <SidebarTrigger className="" />
<Separator orientation="vertical" className="mr-2 h-4" /> <Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink href="#">Crime Management</BreadcrumbLink> <BreadcrumbLink href="#">Sigap - v</BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>Crime Overview</BreadcrumbPage> <BreadcrumbPage>Map</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
</div> </div>
</nav>
{/* Header with action bar and other controls */}
<header className="flex h-16 shrink-0 items-center justify-between border-b px-4 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="w-[300px]">
<ActionSearchBar />
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<InboxDrawer showTitle={true} showAvatar={false} /> <InboxDrawer showTitle={true} showAvatar={false} />
<ThemeSwitcher showTitle={true} /> <ThemeSwitcher showTitle={true} />
@ -77,7 +70,12 @@ export default async function Layout({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
</header> </div>
</nav>
{/* Header with other controls */}
<header className="flex h-12 shrink-0 items-center justify-end border-b px-4 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-8"></header>
<FloatingActionSearchBar />
{children} {children}
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>

View File

@ -1,8 +1,6 @@
"use client" "use client"
import type React from "react" import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react"
import { useState, useEffect } from "react"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { motion, AnimatePresence } from "framer-motion" import { motion, AnimatePresence } from "framer-motion"
import { Search, Send, BarChart2, Globe, Video, PlaneTakeoff, AudioLines } from "lucide-react" import { Search, Send, BarChart2, Globe, Video, PlaneTakeoff, AudioLines } from "lucide-react"
@ -64,20 +62,30 @@ const allActions = [
}, },
] ]
function ActionSearchBar({ actions = allActions }: { actions?: Action[] }) { interface ActionSearchBarProps {
actions?: Action[]
autoFocus?: boolean
isFloating?: boolean
}
const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
({ actions = allActions, autoFocus = false, isFloating = false }, ref) => {
const [query, setQuery] = useState("") const [query, setQuery] = useState("")
const [result, setResult] = useState<SearchResult | null>(null) const [result, setResult] = useState<SearchResult | null>(null)
const [isFocused, setIsFocused] = useState(false) const [isFocused, setIsFocused] = useState(autoFocus)
const [isTyping, setIsTyping] = useState(false)
const [selectedAction, setSelectedAction] = useState<Action | null>(null) const [selectedAction, setSelectedAction] = useState<Action | null>(null)
const debouncedQuery = useDebounce(query, 200) const debouncedQuery = useDebounce(query, 200)
const inputRef = React.useRef<HTMLInputElement>(null)
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
useEffect(() => { useEffect(() => {
if (!isFocused) { if (autoFocus && inputRef.current) {
setResult(null) inputRef.current.focus()
return
} }
}, [autoFocus])
useEffect(() => {
if (!debouncedQuery) { if (!debouncedQuery) {
setResult({ actions: allActions }) setResult({ actions: allActions })
return return
@ -90,11 +98,10 @@ function ActionSearchBar({ actions = allActions }: { actions?: Action[] }) {
}) })
setResult({ actions: filteredActions }) setResult({ actions: filteredActions })
}, [debouncedQuery, isFocused]) }, [debouncedQuery])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value) setQuery(e.target.value)
setIsTyping(true)
} }
const container = { const container = {
@ -141,21 +148,17 @@ function ActionSearchBar({ actions = allActions }: { actions?: Action[] }) {
}, },
} }
const handleFocus = () => {
setSelectedAction(null)
setIsFocused(true)
}
return ( return (
<div className="relative w-full"> <div className={`relative w-full ${isFloating ? "bg-background rounded-lg shadow-lg" : ""}`}>
<div className="relative"> <div className="relative">
<Input <Input
ref={inputRef}
type="text" type="text"
placeholder="What's up?" placeholder="What's up?"
value={query} value={query}
onChange={handleInputChange} onChange={handleInputChange}
onFocus={handleFocus} onFocus={() => setIsFocused(true)}
onBlur={() => setTimeout(() => setIsFocused(false), 200)} onBlur={() => !isFloating && setTimeout(() => setIsFocused(false), 200)}
className="pl-3 pr-9 py-1.5 h-9 text-sm rounded-lg focus-visible:ring-offset-0" className="pl-3 pr-9 py-1.5 h-9 text-sm rounded-lg focus-visible:ring-offset-0"
/> />
<div className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4"> <div className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4">
@ -186,9 +189,9 @@ function ActionSearchBar({ actions = allActions }: { actions?: Action[] }) {
</div> </div>
<AnimatePresence> <AnimatePresence>
{isFocused && result && !selectedAction && ( {(isFocused || isFloating) && result && !selectedAction && (
<motion.div <motion.div
className="absolute left-0 right-0 z-50 border rounded-md shadow-lg overflow-hidden dark:border-gray-800 bg-background mt-1" className={`${isFloating ? "relative mt-2" : "absolute left-0 right-0 z-50"} border rounded-md shadow-lg overflow-hidden dark:border-gray-800 bg-background`}
variants={container} variants={container}
initial="hidden" initial="hidden"
animate="show" animate="show"
@ -228,7 +231,10 @@ function ActionSearchBar({ actions = allActions }: { actions?: Action[] }) {
</AnimatePresence> </AnimatePresence>
</div> </div>
) )
} },
)
ActionSearchBar.displayName = "ActionSearchBar"
export default ActionSearchBar export default ActionSearchBar

View File

@ -0,0 +1,60 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import ActionSearchBar from "@/components/action-search-bar";
export default function FloatingActionSearchBar() {
const [isOpen, setIsOpen] = useState(false);
const searchBarRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
event.preventDefault();
setIsOpen((prev) => !prev);
} else if (event.key === "Escape") {
setIsOpen(false);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
useEffect(() => {
if (isOpen && searchBarRef.current) {
searchBarRef.current.focus();
}
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-50 flex items-start justify-center bg-background/80 backdrop-blur-sm p-4 sm:p-6 md:p-8 lg:p-40"
onClick={() => setIsOpen(false)}
>
<motion.div
initial={{ scale: 0.95, y: -20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.95, y: -20 }}
transition={{ duration: 0.2 }}
className="w-full max-w-lg sm:max-w-xl md:max-w-2xl"
onClick={(e) => e.stopPropagation()}
>
<ActionSearchBar
ref={searchBarRef}
autoFocus={true}
isFloating={true}
/>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}