201 lines · 6.1 KB · TypeScript
RawDownload
import { useCallback, useEffect, useMemo, useState } from "react";import { useNavigate } from "@tanstack/react-router";import { useAtomValue, Result } from "@effect-atom/atom-react";import { PlusIcon } from "lucide-react";import { SourceFavicon } from "./source-favicon";import { sourcesAtom } from "../api/atoms";import { useScope } from "../hooks/use-scope";import type { SourcePlugin } from "../plugins/source-plugin";import {  CommandDialog,  CommandEmpty,  CommandGroup,  CommandInput,  CommandItem,  CommandList,  CommandSeparator,  CommandShortcut,} from "./command"; // ---------------------------------------------------------------------------// CommandPalette — global ⌘K navigator.//// Order of entries://   1. Connected sources (priority, shown first)//   2. Add <Plugin> actions for each available source plugin//   3. Popular sources (plugin presets)// --------------------------------------------------------------------------- export function CommandPalette(props: { sourcePlugins: readonly SourcePlugin[] }) {  const { sourcePlugins } = props;  const [open, setOpen] = useState(false);  const navigate = useNavigate();  const scopeId = useScope();  const sourcesResult = useAtomValue(sourcesAtom(scopeId));   // Toggle with ⌘K / Ctrl+K  useEffect(() => {    const onKeyDown = (e: KeyboardEvent) => {      if (e.key === "k" && (e.metaKey || e.ctrlKey)) {        e.preventDefault();        setOpen((o) => !o);      }    };    document.addEventListener("keydown", onKeyDown);    return () => document.removeEventListener("keydown", onKeyDown);  }, []);   const connectedSources = useMemo(    () =>      Result.match(sourcesResult, {        onInitial: () =>          [] as Array<{            id: string;            name: string;            kind: string;            url?: string;            runtime?: boolean;          }>,        onFailure: () =>          [] as Array<{            id: string;            name: string;            kind: string;            url?: string;            runtime?: boolean;          }>,        onSuccess: ({ value }) => value.filter((s) => !s.runtime),      }),    [sourcesResult],  );   const presetEntries = useMemo(() => {    const entries: Array<{      pluginKey: string;      pluginLabel: string;      presetId: string;      presetName: string;      presetSummary?: string;      presetUrl?: string;      presetIcon?: string;    }> = [];    for (const plugin of sourcePlugins) {      for (const preset of plugin.presets ?? []) {        entries.push({          pluginKey: plugin.key,          pluginLabel: plugin.label,          presetId: preset.id,          presetName: preset.name,          presetSummary: preset.summary,          presetUrl: preset.url,          presetIcon: preset.icon,        });      }    }    return entries;  }, [sourcePlugins]);   const close = useCallback(() => setOpen(false), []);   const goToSource = useCallback(    (id: string) => {      close();      void navigate({ to: "/sources/$namespace", params: { namespace: id } });    },    [close, navigate],  );   const goToAdd = useCallback(    (pluginKey: string) => {      close();      void navigate({        to: "/sources/add/$pluginKey",        params: { pluginKey },      });    },    [close, navigate],  );   const goToPreset = useCallback(    (pluginKey: string, presetId: string, presetUrl?: string) => {      close();      const search: Record<string, string> = { preset: presetId };      if (presetUrl) search.url = presetUrl;      void navigate({        to: "/sources/add/$pluginKey",        params: { pluginKey },        search,      });    },    [close, navigate],  );   return (    <CommandDialog open={open} onOpenChange={setOpen}>      <CommandInput placeholder="Search sources or jump to add…" />      <CommandList>        <CommandEmpty>No results found.</CommandEmpty>         {connectedSources.length > 0 && (          <CommandGroup heading="Connected">            {connectedSources.map((s) => (              <CommandItem                key={`source-${s.id}`}                value={`connected ${s.name} ${s.id} ${s.kind}`}                onSelect={() => goToSource(s.id)}              >                <SourceFavicon url={s.url} />                <span className="flex-1 truncate">{s.name}</span>                <CommandShortcut>{s.kind}</CommandShortcut>              </CommandItem>            ))}          </CommandGroup>        )}         {connectedSources.length > 0 && sourcePlugins.length > 0 && <CommandSeparator />}         {sourcePlugins.length > 0 && (          <CommandGroup heading="Add source">            {sourcePlugins.map((plugin) => (              <CommandItem                key={`add-${plugin.key}`}                value={`add ${plugin.label} ${plugin.key}`}                onSelect={() => goToAdd(plugin.key)}              >                <PlusIcon />                <span className="flex-1 truncate">Add {plugin.label}</span>              </CommandItem>            ))}          </CommandGroup>        )}         {presetEntries.length > 0 && <CommandSeparator />}         {presetEntries.length > 0 && (          <CommandGroup heading="Popular sources">            {presetEntries.map((e) => (              <CommandItem                key={`preset-${e.pluginKey}-${e.presetId}`}                value={`preset ${e.presetName} ${e.presetSummary ?? ""} ${e.pluginLabel}`}                onSelect={() => goToPreset(e.pluginKey, e.presetId, e.presetUrl)}              >                {e.presetIcon ? (                  <img                    src={e.presetIcon}                    alt=""                    className="size-4 shrink-0 object-contain"                    loading="lazy"                  />                ) : (                  <span aria-hidden className="size-4 shrink-0 rounded-sm bg-muted-foreground/20" />                )}                <span className="flex-1 truncate">{e.presetName}</span>                <CommandShortcut>{e.pluginLabel}</CommandShortcut>              </CommandItem>            ))}          </CommandGroup>        )}      </CommandList>    </CommandDialog>  );}