feat: add initial command pallete
This commit is contained in:
		
							
								
								
									
										159
									
								
								islands/KMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								islands/KMenu.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| 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"; | ||||
|  | ||||
| function filterMenu(menu: MenuEntry[], activeMenu?: string) { | ||||
|   return menu; | ||||
| } | ||||
|  | ||||
| const KMenuEntry = ( | ||||
|   { entry, activeIndex, index }: { | ||||
|     entry: MenuEntry; | ||||
|     activeIndex: Signal<number>; | ||||
|     index: number; | ||||
|   }, | ||||
| ) => { | ||||
|   return ( | ||||
|     <div | ||||
|       onClick={() => activeIndex.value = index} | ||||
|       class={`px-4 py-2 ${ | ||||
|         activeIndex.value === index | ||||
|           ? "bg-gray-100 text-gray-900" | ||||
|           : "text-gray-400" | ||||
|       }`} | ||||
|     > | ||||
|       {entry.title} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| 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">("normal"); | ||||
|  | ||||
|   const activeIndex = useSignal(-1); | ||||
|  | ||||
|   const visible = useSignal(false); | ||||
|   const input = useRef<HTMLInputElement>(null); | ||||
|   const commandInput = useSignal(""); | ||||
|  | ||||
|   function activateEntry(menuEntry?: MenuEntry) { | ||||
|     if (!menuEntry) return; | ||||
|  | ||||
|     menuEntry.cb({ | ||||
|       activeMenu: activeMenuType, | ||||
|       menus, | ||||
|       activeState, | ||||
|       visible, | ||||
|     }, context); | ||||
|   } | ||||
|  | ||||
|   useEventListener("keydown", (ev: KeyboardEvent) => { | ||||
|     if (ev.key === "/" && ev.ctrlKey) { | ||||
|       visible.value = !visible.value; | ||||
|     } | ||||
|  | ||||
|     if (ev.key === "ArrowDown") { | ||||
|       const entries = filterMenu(activeMenu.entries); | ||||
|       const index = activeIndex.value; | ||||
|       if (index + 1 >= entries.length) { | ||||
|         activeIndex.value = 0; | ||||
|       } else { | ||||
|         activeIndex.value += 1; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (ev.key === "Enter") { | ||||
|       const entries = filterMenu(activeMenu.entries); | ||||
|       activateEntry(entries[activeIndex.value]); | ||||
|     } | ||||
|  | ||||
|     if (ev.key === "ArrowUp") { | ||||
|       const entries = filterMenu(activeMenu.entries); | ||||
|       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 ( | ||||
|     <div | ||||
|       class={`opacity-${visible.value ? 100 : 0} pointer-events-${ | ||||
|         visible.value ? "auto" : "none" | ||||
|       } transition grid place-items-center w-full h-full fixed top-0 left-0 z-50`} | ||||
|       style={{ background: "#1f1f1f88" }} | ||||
|     > | ||||
|       <div | ||||
|         class={`relative w-1/2 max-h-64 rounded-2xl shadow-2xl nnoisy-gradient overflow-hidden after:opacity-10 border border-gray-500`} | ||||
|         style={{ background: "#1f1f1f" }} | ||||
|       > | ||||
|         <div | ||||
|           class="grid h-12 text-gray-400 border-b border-gray-500 " | ||||
|           style={{ gridTemplateColumns: "4em 1fr" }} | ||||
|         > | ||||
|           {activeState.value === "normal" && | ||||
|             ( | ||||
|               <> | ||||
|                 <div class="grid place-items-center border-r border-gray-500"> | ||||
|                   <span class="text-white"> | ||||
|                     {activeMenu.title} | ||||
|                   </span> | ||||
|                 </div> | ||||
|                 <input | ||||
|                   ref={input} | ||||
|                   onInput={() => { | ||||
|                     commandInput.value = input.current?.value || ""; | ||||
|                   }} | ||||
|                   placeholder="Command" | ||||
|                   class="bg-transparent color pl-4 border-0" | ||||
|                 /> | ||||
|               </> | ||||
|             )} | ||||
|           {activeState.value === "loading" && ( | ||||
|             <div class="p-4"> | ||||
|               Loading... | ||||
|             </div> | ||||
|           )} | ||||
|         </div> | ||||
|         {activeState.value === "normal" && | ||||
|           ( | ||||
|             <div> | ||||
|               {filterMenu(activeMenu.entries, input.current?.value).map( | ||||
|                 (k, index) => { | ||||
|                   return ( | ||||
|                     <KMenuEntry | ||||
|                       entry={k} | ||||
|                       activeIndex={activeIndex} | ||||
|                       index={index} | ||||
|                     /> | ||||
|                   ); | ||||
|                 }, | ||||
|               )} | ||||
|             </div> | ||||
|           )} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user