import { Signal, useSignal } from "@preact/signals"; import { useRef } from "preact/hooks"; import { useEventListener } from "@lib/hooks/useEventListener.ts"; import { menus } from "@islands/KMenu/commands.ts"; import { MenuEntry } from "@islands/KMenu/types.ts"; const KMenuEntry = ( { entry, activeIndex, index }: { entry: MenuEntry; activeIndex: Signal; index: number; }, ) => { return (
activeIndex.value = index} class={`px-4 py-2 ${ activeIndex.value === index ? "bg-gray-100 text-gray-900" : "text-gray-400" }`} > {entry.title}
); }; export const KMenu = ( { type = "main", context }: { context: unknown; type: keyof typeof menus }, ) => { const activeMenuType = useSignal(type); const activeMenu = menus[activeMenuType.value || "main"]; const activeState = useSignal<"normal" | "loading" | "error" | "input">( "normal", ); const activeIndex = useSignal(-1); const input = useRef(null); const commandInput = useSignal(""); const visible = useSignal(false); if (visible.value === false) { setTimeout(() => { activeMenuType.value = "main"; activeState.value = "normal"; if (input.current) { input.current.value = ""; } commandInput.value = ""; }, 100); } const entries = activeMenu?.entries?.filter((entry) => { const entryVisible = typeof entry["visible"] === "boolean" ? entry.visible : typeof entry["visible"] === "function" ? entry.visible?.(context) : true; const search = commandInput?.value?.toLowerCase(); const searchMatches = entry.title.toLowerCase().includes(search) || entry.meta?.toLowerCase()?.includes(search); return entryVisible && searchMatches; }); if (entries.length === 1) { activeIndex.value = 0; } function activateEntry(menuEntry?: MenuEntry) { if (!menuEntry) return; menuEntry.cb({ activeMenu: activeMenuType, menus, activeState, commandInput, visible, }, context); } useEventListener("keydown", (ev: KeyboardEvent) => { if (ev.key === "/" && ev.ctrlKey) { visible.value = !visible.value; } if (ev.key === "ArrowDown") { const index = activeIndex.value; if (index + 1 >= entries.length) { activeIndex.value = 0; } else { activeIndex.value += 1; } } if (ev.key === "Enter") { activateEntry(entries[activeIndex.value]); } if (ev.key === "ArrowUp") { const index = activeIndex.value; if (index - 1 < 0) { activeIndex.value = entries.length - 1; } else { activeIndex.value -= 1; } } if (ev.key === "Escape") { visible.value = false; if (input.current) { input.current.value = ""; } } if (visible.value) { input.current?.focus(); } else { input.current?.blur(); } }); return (
{(activeState.value === "normal" || activeState.value === "input") && ( <>
{activeMenu.title}
{ commandInput.value = input.current?.value || ""; }} placeholder="Command" class="bg-transparent color pl-4 border-0" /> )} {activeState.value === "loading" && (
Loading...
)}
{activeState.value === "normal" && (
{entries?.length === 0 && (
No Entries
)} {entries.map( (k, index) => { return ( ); }, )}
)}
); };