fix(nav): restore sidebar collapse state from cookie; cmd+k search shows tools only while typing; mobile header search
Build & Push Docker Image / build (push) Successful in 3m22s
Build & Push Docker Image / build (push) Successful in 3m22s
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useListTools, getListToolsQueryKey } from "@workspace/api-client-react";
|
import { useListTools, getListToolsQueryKey } from "@workspace/api-client-react";
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
Wrench,
|
Wrench,
|
||||||
Star,
|
Star,
|
||||||
History,
|
History,
|
||||||
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
const openRequesters = new Set<() => void>();
|
const openRequesters = new Set<() => void>();
|
||||||
@@ -37,7 +38,6 @@ export function CommandPalette() {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [recent, setRecent] = useState(() => getRecentTools());
|
const [recent, setRecent] = useState(() => getRecentTools());
|
||||||
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const request = () => setOpen(true);
|
const request = () => setOpen(true);
|
||||||
@@ -66,12 +66,14 @@ export function CommandPalette() {
|
|||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
const searching = search.trim().length > 0;
|
||||||
|
|
||||||
const { data: tools } = useListTools(
|
const { data: tools } = useListTools(
|
||||||
search ? { search } : undefined,
|
searching ? { search } : undefined,
|
||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
queryKey: getListToolsQueryKey(search ? { search } : undefined),
|
queryKey: getListToolsQueryKey(searching ? { search } : undefined),
|
||||||
enabled: open && search.trim().length > 0,
|
enabled: open && searching,
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -84,24 +86,23 @@ export function CommandPalette() {
|
|||||||
|
|
||||||
const showTrash = hasFeature("trash") || isAdmin;
|
const showTrash = hasFeature("trash") || isAdmin;
|
||||||
const showWatchlist = (hasFeature("watchlist") || isAdmin) && isAuthenticated;
|
const showWatchlist = (hasFeature("watchlist") || isAdmin) && isAuthenticated;
|
||||||
const searching = search.trim().length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
|
autoFocus
|
||||||
placeholder={`${t("common.search")}…`}
|
placeholder={`${t("common.search")}…`}
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={(v) => {
|
onValueChange={setSearch}
|
||||||
setSearch(v);
|
|
||||||
if (searchTimer.current) clearTimeout(searchTimer.current);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
{searching ? t("command.noResults", { query: search }) : t("command.startTyping")}
|
{searching ? t("command.noResults", { query: search }) : t("command.startTyping")}
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
{!searching && recent.length > 0 && (
|
{!searching && (
|
||||||
|
<>
|
||||||
|
{recent.length > 0 && (
|
||||||
<CommandGroup heading={t("command.recent")}>
|
<CommandGroup heading={t("command.recent")}>
|
||||||
{recent.map((tool) => (
|
{recent.map((tool) => (
|
||||||
<CommandItem key={tool.id} onSelect={() => go(`/tools/${tool.id}`)}>
|
<CommandItem key={tool.id} onSelect={() => go(`/tools/${tool.id}`)}>
|
||||||
@@ -111,7 +112,6 @@ export function CommandPalette() {
|
|||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CommandGroup heading={t("command.navigate")}>
|
<CommandGroup heading={t("command.navigate")}>
|
||||||
<CommandItem onSelect={() => go("/tools")}>
|
<CommandItem onSelect={() => go("/tools")}>
|
||||||
<Compass className="mr-2 h-4 w-4" /> {t("nav.browseTools")}
|
<Compass className="mr-2 h-4 w-4" /> {t("nav.browseTools")}
|
||||||
@@ -138,13 +138,16 @@ export function CommandPalette() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
)}
|
)}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{searching && (
|
{searching && (
|
||||||
<>
|
<>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup heading={t("command.tools")}>
|
<CommandGroup heading={t("command.tools")}>
|
||||||
{(tools ?? []).slice(0, 10).map((tool) => (
|
{(tools ?? []).slice(0, 10).map((tool) => (
|
||||||
<CommandItem key={tool.id} onSelect={() => go(`/tools/${tool.id}`)}>
|
<CommandItem key={tool.id} onSelect={() => go(`/tools/${tool.id}`)}>
|
||||||
<Wrench className="mr-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
<Search className="mr-2 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
<span className="truncate">{tool.name}</span>
|
<span className="truncate">{tool.name}</span>
|
||||||
<span className="ml-auto text-xs text-muted-foreground shrink-0">
|
<span className="ml-auto text-xs text-muted-foreground shrink-0">
|
||||||
{tool.avgCombined ? `${tool.avgCombined.toFixed(1)}★` : ""}
|
{tool.avgCombined ? `${tool.avgCombined.toFixed(1)}★` : ""}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { LayoutDashboard, Wrench, PlusCircle, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Trash2, Bookmark } from "lucide-react";
|
import { LayoutDashboard, Wrench, PlusCircle, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Trash2, Bookmark, Search } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { CommandPalette } from "@/components/command-palette";
|
import { CommandPalette, requestOpenCommandPalette } from "@/components/command-palette";
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
import { UserMenu } from "@/components/user-menu";
|
import { UserMenu } from "@/components/user-menu";
|
||||||
import { Breadcrumbs } from "@/components/breadcrumbs";
|
import { Breadcrumbs } from "@/components/breadcrumbs";
|
||||||
@@ -149,6 +149,16 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
<HeaderSearch />
|
<HeaderSearch />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 md:hidden"
|
||||||
|
onClick={requestOpenCommandPalette}
|
||||||
|
aria-label={t("nav.search")}
|
||||||
|
data-testid="button-search-mobile"
|
||||||
|
>
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ function useSidebar() {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSidebarCookie(): boolean | null {
|
||||||
|
if (typeof document === "undefined") return null;
|
||||||
|
const match = document.cookie.match(
|
||||||
|
new RegExp(`(?:^|; )${SIDEBAR_COOKIE_NAME}=([^;]*)`)
|
||||||
|
);
|
||||||
|
if (!match) return null;
|
||||||
|
return match[1] === "true";
|
||||||
|
}
|
||||||
|
|
||||||
function SidebarProvider({
|
function SidebarProvider({
|
||||||
defaultOpen = true,
|
defaultOpen = true,
|
||||||
open: openProp,
|
open: openProp,
|
||||||
@@ -71,7 +80,10 @@ function SidebarProvider({
|
|||||||
|
|
||||||
// This is the internal state of the sidebar.
|
// This is the internal state of the sidebar.
|
||||||
// We use openProp and setOpenProp for control from outside the component.
|
// We use openProp and setOpenProp for control from outside the component.
|
||||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
const [_open, _setOpen] = React.useState(() => {
|
||||||
|
const stored = getSidebarCookie()
|
||||||
|
return stored !== null ? stored : defaultOpen
|
||||||
|
})
|
||||||
const open = openProp ?? _open
|
const open = openProp ?? _open
|
||||||
const setOpen = React.useCallback(
|
const setOpen = React.useCallback(
|
||||||
(value: boolean | ((value: boolean) => boolean)) => {
|
(value: boolean | ((value: boolean) => boolean)) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user