From ab24f99b90a73646b8fa480e757ea77eecae394f Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 2 Aug 2026 15:08:37 +0200 Subject: [PATCH] fix(nav): restore sidebar collapse state from cookie; cmd+k search shows tools only while typing; mobile header search --- .../src/components/command-palette.tsx | 93 ++++++++++--------- artifacts/toolrate/src/components/layout.tsx | 14 ++- .../toolrate/src/components/ui/sidebar.tsx | 14 ++- 3 files changed, 73 insertions(+), 48 deletions(-) diff --git a/artifacts/toolrate/src/components/command-palette.tsx b/artifacts/toolrate/src/components/command-palette.tsx index 0b5eda2..7f4e39c 100644 --- a/artifacts/toolrate/src/components/command-palette.tsx +++ b/artifacts/toolrate/src/components/command-palette.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import { useLocation } from "wouter"; import { useListTools, getListToolsQueryKey } from "@workspace/api-client-react"; import { @@ -22,6 +22,7 @@ import { Wrench, Star, History, + Search, } from "lucide-react"; const openRequesters = new Set<() => void>(); @@ -37,7 +38,6 @@ export function CommandPalette() { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const [recent, setRecent] = useState(() => getRecentTools()); - const searchTimer = useRef | null>(null); useEffect(() => { const request = () => setOpen(true); @@ -66,12 +66,14 @@ export function CommandPalette() { } }, [open]); + const searching = search.trim().length > 0; + const { data: tools } = useListTools( - search ? { search } : undefined, + searching ? { search } : undefined, { query: { - queryKey: getListToolsQueryKey(search ? { search } : undefined), - enabled: open && search.trim().length > 0, + queryKey: getListToolsQueryKey(searching ? { search } : undefined), + enabled: open && searching, staleTime: 30_000, }, }, @@ -84,67 +86,68 @@ export function CommandPalette() { const showTrash = hasFeature("trash") || isAdmin; const showWatchlist = (hasFeature("watchlist") || isAdmin) && isAuthenticated; - const searching = search.trim().length > 0; return ( { - setSearch(v); - if (searchTimer.current) clearTimeout(searchTimer.current); - }} + onValueChange={setSearch} /> {searching ? t("command.noResults", { query: search }) : t("command.startTyping")} - {!searching && recent.length > 0 && ( - - {recent.map((tool) => ( - go(`/tools/${tool.id}`)}> - - {tool.name} + {!searching && ( + <> + {recent.length > 0 && ( + + {recent.map((tool) => ( + go(`/tools/${tool.id}`)}> + + {tool.name} + + ))} + + )} + + go("/tools")}> + {t("nav.browseTools")} - ))} - + go("/tools/new")}> + {t("nav.addTool")} + + go("/analytics")}> + {t("nav.analytics")} + + {showWatchlist && ( + go("/watchlist")}> + {t("nav.watchlist")} + + )} + {showTrash && ( + go("/trash")}> + {t("nav.trash")} + + )} + {isAdmin && ( + go("/admin")}> + {t("nav.admin")} + + )} + + )} - - go("/tools")}> - {t("nav.browseTools")} - - go("/tools/new")}> - {t("nav.addTool")} - - go("/analytics")}> - {t("nav.analytics")} - - {showWatchlist && ( - go("/watchlist")}> - {t("nav.watchlist")} - - )} - {showTrash && ( - go("/trash")}> - {t("nav.trash")} - - )} - {isAdmin && ( - go("/admin")}> - {t("nav.admin")} - - )} - {searching && ( <> {(tools ?? []).slice(0, 10).map((tool) => ( go(`/tools/${tool.id}`)}> - + {tool.name} {tool.avgCombined ? `${tool.avgCombined.toFixed(1)}★` : ""} diff --git a/artifacts/toolrate/src/components/layout.tsx b/artifacts/toolrate/src/components/layout.tsx index b871be7..acd96ac 100644 --- a/artifacts/toolrate/src/components/layout.tsx +++ b/artifacts/toolrate/src/components/layout.tsx @@ -1,11 +1,11 @@ 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 { useAuth } from "@/hooks/use-auth"; import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react"; import { Button } from "@/components/ui/button"; 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 { UserMenu } from "@/components/user-menu"; import { Breadcrumbs } from "@/components/breadcrumbs"; @@ -149,6 +149,16 @@ export function Layout({ children }: { children: React.ReactNode }) {
+ {!isLoading && ( diff --git a/artifacts/toolrate/src/components/ui/sidebar.tsx b/artifacts/toolrate/src/components/ui/sidebar.tsx index 41362f6..1bd0a82 100644 --- a/artifacts/toolrate/src/components/ui/sidebar.tsx +++ b/artifacts/toolrate/src/components/ui/sidebar.tsx @@ -53,6 +53,15 @@ function useSidebar() { 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({ defaultOpen = true, open: openProp, @@ -71,7 +80,10 @@ function SidebarProvider({ // This is the internal state of the sidebar. // 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 setOpen = React.useCallback( (value: boolean | ((value: boolean) => boolean)) => {