ShortcutsTable.svelte RawHistoryFolder
1<script lang="ts">
2 import { Check, Plus, Trash2, X } from "@lucide/svelte";
3 import { Button } from "$lib/components/ui/button";
4 import { Input } from "$lib/components/ui/input";
5 import * as Table from "$lib/components/ui/table";
6
7 type SearchShortcut = {
8 url: string;
9 description: string;
10 };
11
12 interface Props {
13 shortcuts: Record<string, SearchShortcut>;
14 onUpdate: (shortcuts: Record<string, SearchShortcut>) => void;
15 }
16
17 let { shortcuts, onUpdate }: Props = $props();
18
19 let editingKey = $state<string | null>(null);
20 let draftKey = $state("");
21 let draftUrl = $state("");
22 let draftDescription = $state("");
23
24 let newKey = $state("");
25 let newUrl = $state("");
26 let newDescription = $state("");
27
28 const entries = $derived(
29 Object.entries(shortcuts ?? {}).sort(([a], [b]) => a.localeCompare(b)),
30 );
31
32 function startEdit(key: string, shortcut: SearchShortcut) {
33 editingKey = key;
34 draftKey = key;
35 draftUrl = shortcut.url;
36 draftDescription = shortcut.description;
37 }
38
39 function cancelEdit() {
40 editingKey = null;
41 draftKey = "";
42 draftUrl = "";
43 draftDescription = "";
44 }
45
46 function commitEdit() {
47 if (!editingKey) return;
48 const nextKey = draftKey.trim();
49 const nextUrl = draftUrl.trim();
50 const nextDescription = draftDescription.trim();
51
52 const updated: Record<string, SearchShortcut> = { ...shortcuts };
53 delete updated[editingKey];
54 if (nextKey) {
55 updated[nextKey] = { url: nextUrl, description: nextDescription };
56 }
57 onUpdate(updated);
58 cancelEdit();
59 }
60
61 function removeShortcut(key: string) {
62 const updated: Record<string, SearchShortcut> = { ...shortcuts };
63 delete updated[key];
64 onUpdate(updated);
65 }
66
67 function addShortcut() {
68 const key = newKey.trim();
69 if (!key) return;
70 const updated: Record<string, SearchShortcut> = { ...shortcuts };
71 updated[key] = { url: newUrl.trim(), description: newDescription.trim() };
72 onUpdate(updated);
73 newKey = "";
74 newUrl = "";
75 newDescription = "";
76 }
77</script>
78
79<div class="space-y-4">
80 <Table.Root class="rounded-md border border-border">
81 <Table.Header>
82 <Table.Row>
83 <Table.Head class="w-[140px]">Prefix</Table.Head>
84 <Table.Head>URL Template</Table.Head>
85 <Table.Head>Description</Table.Head>
86 <Table.Head class="w-[160px] text-right">Actions</Table.Head>
87 </Table.Row>
88 </Table.Header>
89 <Table.Body>
90 {#if entries.length === 0}
91 <Table.Row>
92 <Table.Cell colspan={4} class="py-8 text-center text-sm text-muted-foreground">
93 No shortcuts yet. Add one below.
94 </Table.Cell>
95 </Table.Row>
96 {/if}
97 {#each entries as [key, shortcut] (key)}
98 <Table.Row>
99 {#if editingKey === key}
100 <Table.Cell>
101 <Input bind:value={draftKey} class="h-8" />
102 </Table.Cell>
103 <Table.Cell>
104 <Input bind:value={draftUrl} class="h-8" />
105 </Table.Cell>
106 <Table.Cell>
107 <Input bind:value={draftDescription} class="h-8" />
108 </Table.Cell>
109 <Table.Cell class="text-right">
110 <div class="flex justify-end gap-2">
111 <Button variant="ghost" size="icon" onclick={commitEdit} type="button" aria-label="Save">
112 <Check size={16} />
113 </Button>
114 <Button variant="ghost" size="icon" onclick={cancelEdit} type="button" aria-label="Cancel">
115 <X size={16} />
116 </Button>
117 </div>
118 </Table.Cell>
119 {:else}
120 <Table.Cell class="font-mono text-sm">{key}</Table.Cell>
121 <Table.Cell class="truncate max-w-[360px]">{shortcut.url}</Table.Cell>
122 <Table.Cell class="text-muted-foreground">{shortcut.description}</Table.Cell>
123 <Table.Cell class="text-right">
124 <div class="flex justify-end gap-2">
125 <Button
126 variant="outline"
127 size="sm"
128 onclick={() => startEdit(key, shortcut)}
129 type="button"
130 >
131 Edit
132 </Button>
133 <Button
134 variant="ghost"
135 size="icon"
136 onclick={() => removeShortcut(key)}
137 type="button"
138 aria-label={`Delete ${key}`}
139 >
140 <Trash2 size={16} />
141 </Button>
142 </div>
143 </Table.Cell>
144 {/if}
145 </Table.Row>
146 {/each}
147 </Table.Body>
148 </Table.Root>
149
150 <div class="grid gap-3 md:grid-cols-[140px_1fr_1fr_140px] items-end">
151 <div class="space-y-1">
152 <span class="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">Prefix</span>
153 <Input bind:value={newKey} placeholder="g" />
154 </div>
155 <div class="space-y-1">
156 <span class="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">URL Template</span>
157 <Input bind:value={newUrl} placeholder="https://www.google.com/search?q=%s" />
158 </div>
159 <div class="space-y-1">
160 <span class="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">Description</span>
161 <Input bind:value={newDescription} placeholder="Google" />
162 </div>
163 <div class="flex md:justify-end">
164 <Button variant="default" onclick={addShortcut} type="button" class="w-full md:w-auto">
165 <Plus size={16} />
166 Add Shortcut
167 </Button>
168 </div>
169 </div>
170</div>