your personal website on atproto - mirror blento.app
at custom-domains-editing 192 lines 6.4 kB view raw
1<script lang="ts"> 2 import { AllCardDefinitions } from '$lib/cards'; 3 import type { CardDefinition } from '$lib/cards/types'; 4 import { Command, Dialog } from 'bits-ui'; 5 import { isTyping } from '$lib/helper'; 6 7 const CardDefGroups = [ 8 'Core', 9 ...Array.from( 10 new Set( 11 AllCardDefinitions.map((cardDef) => cardDef.groups) 12 .flat() 13 .filter((g) => g) 14 ) 15 ) 16 .sort() 17 .filter((g) => g !== 'Core') 18 ]; 19 20 let { 21 open = $bindable(false), 22 onselect, 23 onlink 24 }: { 25 open: boolean; 26 onselect: (cardDef: CardDefinition) => void; 27 onlink?: (url: string, cardDef: CardDefinition) => void; 28 } = $props(); 29 30 let searchValue = $state(''); 31 32 let normalizedUrl = $derived.by(() => { 33 if (!searchValue || searchValue.length < 8) return ''; 34 try { 35 const val = searchValue.trim(); 36 const urlStr = val.startsWith('http') ? val : `https://${val}`; 37 const url = new URL(urlStr); 38 if (!url.hostname.includes('.')) return ''; 39 return urlStr; 40 } catch { 41 return ''; 42 } 43 }); 44 45 let urlMatchingCards = $derived.by(() => { 46 if (!normalizedUrl) return []; 47 return AllCardDefinitions.filter((d) => d.onUrlHandler) 48 .filter((d) => { 49 try { 50 const testItem = { cardData: {} }; 51 return d.onUrlHandler!(normalizedUrl, testItem as any); 52 } catch { 53 return false; 54 } 55 }) 56 .toSorted((a, b) => (b.urlHandlerPriority ?? 0) - (a.urlHandlerPriority ?? 0)); 57 }); 58 59 function selectUrl(cardDef: CardDefinition) { 60 const url = normalizedUrl; 61 open = false; 62 searchValue = ''; 63 onlink?.(url, cardDef); 64 } 65 66 function commandFilter(value: string, search: string, keywords?: string[]): number { 67 if (value.startsWith('url:')) return 1; 68 const s = search.toLowerCase(); 69 for (const t of [value, ...(keywords ?? [])]) { 70 if (t.toLowerCase().includes(s)) return 1; 71 } 72 return 0; 73 } 74 75 function handleKeydown(e: KeyboardEvent) { 76 if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { 77 e.preventDefault(); 78 open = !open; 79 } 80 if (e.key === '+' && !isTyping()) { 81 e.preventDefault(); 82 open = true; 83 } 84 } 85</script> 86 87<svelte:document onkeydown={handleKeydown} /> 88 89<Dialog.Root bind:open> 90 <Dialog.Portal> 91 <Dialog.Overlay 92 class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80" 93 /> 94 <Dialog.Content 95 class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-36 left-[50%] z-50 w-full max-w-[94%] translate-x-[-50%] outline-hidden sm:max-w-lg md:w-full" 96 > 97 <Dialog.Title class="sr-only">Command Menu</Dialog.Title> 98 <Dialog.Description class="sr-only"> 99 This is the command menu. Use the arrow keys to navigate and press ⌘K to open the search 100 bar. 101 </Dialog.Description> 102 <Command.Root 103 filter={commandFilter} 104 class="border-base-200 dark:border-base-800 mx-auto flex h-full w-full max-w-[90vw] flex-col overflow-hidden rounded-2xl border bg-white dark:bg-black" 105 > 106 <Command.Input 107 class="focus-override placeholder:text-base-900/50 dark:placeholder:text-base-50/50 border-base-200 dark:border-base-800 bg-base-100 mx-1 mt-1 inline-flex truncate rounded-2xl rounded-tl-2xl px-4 text-sm transition-colors focus:ring-0 focus:outline-hidden dark:bg-black" 108 placeholder="Search for a card or paste a link..." 109 oninput={(e) => { 110 searchValue = e.currentTarget.value; 111 }} 112 /> 113 114 <Command.List 115 class="focus:outline-accent-500/50 max-h-[50vh] overflow-x-hidden overflow-y-auto rounded-br-2xl rounded-bl-2xl bg-white px-2 pb-2 focus:border-0 dark:bg-black" 116 > 117 <Command.Viewport> 118 <Command.Empty 119 class="text-base-900 dark:text-base-100 flex w-full items-center justify-center pt-8 pb-6 text-sm" 120 > 121 No results found. 122 </Command.Empty> 123 124 {#if urlMatchingCards.length > 0} 125 <Command.Group> 126 <Command.GroupHeading 127 class="text-base-600 dark:text-base-400 px-3 pt-3 pb-2 text-xs" 128 > 129 Add from link 130 </Command.GroupHeading> 131 <Command.GroupItems> 132 {#each urlMatchingCards as cardDef (cardDef.type)} 133 <Command.Item 134 value="url:{cardDef.type}" 135 onSelect={() => { 136 selectUrl(cardDef); 137 }} 138 class="rounded-button data-selected:bg-accent-500/10 flex h-10 cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm outline-hidden select-none" 139 > 140 {#if cardDef.icon} 141 <div class="text-base-700 dark:text-base-300"> 142 {@html cardDef.icon} 143 </div> 144 {/if} 145 {cardDef.name} 146 </Command.Item> 147 {/each} 148 </Command.GroupItems> 149 </Command.Group> 150 <Command.Separator class="bg-base-900/5 dark:bg-base-50/5 my-1 h-px w-full" /> 151 {/if} 152 153 {#each CardDefGroups as group, index (group)} 154 {#if group && AllCardDefinitions.some((cardDef) => cardDef.groups?.includes(group))} 155 <Command.Group> 156 <Command.GroupHeading 157 class="text-base-600 dark:text-base-400 px-3 pt-4 pb-2 text-xs" 158 > 159 {group} 160 </Command.GroupHeading> 161 <Command.GroupItems> 162 {#each AllCardDefinitions.filter( (cardDef) => cardDef.groups?.includes(group) ) as cardDef (cardDef.type)} 163 <Command.Item 164 onSelect={() => { 165 open = false; 166 searchValue = ''; 167 onselect(cardDef); 168 }} 169 class="rounded-button data-selected:bg-accent-500/10 flex h-10 cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm outline-hidden select-none" 170 keywords={[group, cardDef.type, ...(cardDef.keywords || [])]} 171 > 172 {#if cardDef.icon} 173 <div class="text-base-700 dark:text-base-300"> 174 {@html cardDef.icon} 175 </div> 176 {/if} 177 {cardDef.name} 178 </Command.Item> 179 {/each} 180 </Command.GroupItems> 181 </Command.Group> 182 {#if index < CardDefGroups.length - 1} 183 <Command.Separator class="bg-base-900/5 dark:bg-base-50/5 my-1 h-px w-full" /> 184 {/if} 185 {/if} 186 {/each} 187 </Command.Viewport> 188 </Command.List> 189 </Command.Root> 190 </Dialog.Content> 191 </Dialog.Portal> 192</Dialog.Root>