feat(nav): collapsible sidebar + mobile drawer, user menu, breadcrumbs, polished cmd+k, header search
Build & Push Docker Image / build (push) Successful in 2m46s

This commit is contained in:
opencode
2026-08-02 14:55:18 +02:00
parent 2f38889726
commit c6755b586b
9 changed files with 493 additions and 158 deletions
@@ -12,6 +12,7 @@ import {
} from "@/components/ui/command";
import { useAuth } from "@/hooks/use-auth";
import { useTranslation } from "react-i18next";
import { getRecentTools } from "@/lib/recent-tools";
import {
Compass,
PlusCircle,
@@ -20,16 +21,32 @@ import {
ShieldCheck,
Wrench,
Star,
History,
} from "lucide-react";
const openRequesters = new Set<() => void>();
export function requestOpenCommandPalette() {
for (const request of openRequesters) request();
}
export function CommandPalette() {
const [, navigate] = useLocation();
const { t } = useTranslation();
const { isAuthenticated, isAdmin, hasFeature } = useAuth();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [recent, setRecent] = useState(() => getRecentTools());
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const request = () => setOpen(true);
openRequesters.add(request);
return () => {
openRequesters.delete(request);
};
}, []);
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key.toLowerCase() === "k" && (e.metaKey || e.ctrlKey)) {
@@ -42,7 +59,11 @@ export function CommandPalette() {
}, []);
useEffect(() => {
if (!open) setSearch("");
if (!open) {
setSearch("");
} else {
setRecent(getRecentTools());
}
}, [open]);
const { data: tools } = useListTools(
@@ -63,6 +84,7 @@ export function CommandPalette() {
const showTrash = hasFeature("trash") || isAdmin;
const showWatchlist = (hasFeature("watchlist") || isAdmin) && isAuthenticated;
const searching = search.trim().length > 0;
return (
<CommandDialog open={open} onOpenChange={setOpen}>
@@ -76,9 +98,21 @@ export function CommandPalette() {
/>
<CommandList>
<CommandEmpty>
{search ? `No tools found for "${search}".` : "Start typing to search tools."}
{searching ? t("command.noResults", { query: search }) : t("command.startTyping")}
</CommandEmpty>
<CommandGroup heading="Navigate">
{!searching && recent.length > 0 && (
<CommandGroup heading={t("command.recent")}>
{recent.map((tool) => (
<CommandItem key={tool.id} onSelect={() => go(`/tools/${tool.id}`)}>
<History className="mr-2 h-4 w-4 text-muted-foreground" />
<span className="truncate">{tool.name}</span>
</CommandItem>
))}
</CommandGroup>
)}
<CommandGroup heading={t("command.navigate")}>
<CommandItem onSelect={() => go("/tools")}>
<Compass className="mr-2 h-4 w-4" /> {t("nav.browseTools")}
</CommandItem>
@@ -104,10 +138,10 @@ export function CommandPalette() {
</CommandItem>
)}
</CommandGroup>
{search.trim().length > 0 && (
{searching && (
<>
<CommandSeparator />
<CommandGroup heading="Tools">
<CommandGroup heading={t("command.tools")}>
{(tools ?? []).slice(0, 10).map((tool) => (
<CommandItem key={tool.id} onSelect={() => go(`/tools/${tool.id}`)}>
<Wrench className="mr-2 h-4 w-4 shrink-0 text-muted-foreground" />