feat(nav): collapsible sidebar + mobile drawer, user menu, breadcrumbs, polished cmd+k, header search
Build & Push Docker Image / build (push) Successful in 2m46s
Build & Push Docker Image / build (push) Successful in 2m46s
This commit is contained in:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user