FolderList.svelte RawHistoryFolder
1<script lang="ts">
2 import type { Folder, Favorite } from '../types';
3
4 interface Props {
5 folders: Folder[];
6 favorites: Favorite[];
7 selectedFolderId?: number | null;
8 onSelectFolder?: (id: number | null) => void;
9 onCreateFolder?: (name: string) => Promise<void>;
10 }
11
12 let {
13 folders,
14 favorites,
15 selectedFolderId = null,
16 onSelectFolder,
17 onCreateFolder
18 }: Props = $props();
19
20 // Inline creation state
21 let isCreating = $state(false);
22 let newFolderName = $state('');
23 let inputRef = $state<HTMLInputElement | null>(null);
24
25 const startCreating = () => {
26 isCreating = true;
27 newFolderName = '';
28 // Focus input after it renders
29 setTimeout(() => inputRef?.focus(), 0);
30 };
31
32 const cancelCreating = () => {
33 isCreating = false;
34 newFolderName = '';
35 };
36
37 const submitCreate = async () => {
38 const name = newFolderName.trim();
39 if (!name || !onCreateFolder) {
40 cancelCreating();
41 return;
42 }
43 try {
44 await onCreateFolder(name);
45 cancelCreating();
46 } catch (e) {
47 console.error('Failed to create folder:', e);
48 }
49 };
50
51 const handleKeydown = (e: KeyboardEvent) => {
52 if (e.key === 'Enter') {
53 e.preventDefault();
54 submitCreate();
55 } else if (e.key === 'Escape') {
56 cancelCreating();
57 }
58 };
59
60 // Count favorites per folder
61 const getFolderCount = (folderId: number | null): number => {
62 if (folderId === null) {
63 return favorites.length;
64 }
65 return favorites.filter(f => f.folder_id === folderId).length;
66 };
67
68 // Count unfiled favorites
69 const unfiledCount = $derived(favorites.filter(f => f.folder_id === null).length);
70</script>
71
72<div class="folder-list">
73 <div class="folder-header">
74 <span class="header-label">FOLDERS</span>
75 {#if isCreating}
76 <button
77 class="add-folder-btn cancel"
78 type="button"
79 onclick={cancelCreating}
80 title="Cancel"
81 >
82 ×
83 </button>
84 {:else}
85 <button
86 class="add-folder-btn"
87 type="button"
88 onclick={startCreating}
89 title="Create folder"
90 >
91 +
92 </button>
93 {/if}
94 </div>
95
96 {#if isCreating}
97 <div class="create-input-row">
98 <input
99 bind:this={inputRef}
100 bind:value={newFolderName}
101 onkeydown={handleKeydown}
102 onblur={cancelCreating}
103 class="create-input"
104 type="text"
105 placeholder="Folder name..."
106 maxlength="50"
107 />
108 </div>
109 {/if}
110
111 <div class="folder-items">
112 <!-- All favorites -->
113 <button
114 class="folder-item"
115 class:active={selectedFolderId === null}
116 type="button"
117 onclick={() => onSelectFolder?.(null)}
118 >
119 <span class="folder-icon"></span>
120 <span class="folder-name">All</span>
121 <span class="folder-count">{favorites.length}</span>
122 </button>
123
124 <!-- User folders -->
125 {#each folders as folder (folder.id)}
126 <button
127 class="folder-item"
128 class:active={selectedFolderId === folder.id}
129 type="button"
130 onclick={() => onSelectFolder?.(folder.id)}
131 >
132 <span class="folder-icon">{folder.icon || ''}</span>
133 <span class="folder-name">{folder.name}</span>
134 <span class="folder-count">{getFolderCount(folder.id)}</span>
135 </button>
136 {/each}
137
138 <!-- Unfiled -->
139 {#if unfiledCount > 0 && folders.length > 0}
140 <button
141 class="folder-item unfiled"
142 class:active={selectedFolderId === -1}
143 type="button"
144 onclick={() => onSelectFolder?.(-1)}
145 >
146 <span class="folder-icon"></span>
147 <span class="folder-name">Unfiled</span>
148 <span class="folder-count">{unfiledCount}</span>
149 </button>
150 {/if}
151 </div>
152</div>
153
154<style>
155 .folder-list {
156 display: flex;
157 flex-direction: column;
158 border-width: 1px;
159 border-style: solid;
160 border-color: var(--border);
161 background: color-mix(in srgb, var(--background) 92%, var(--card) 8%);
162 }
163
164 .folder-header {
165 display: flex;
166 align-items: center;
167 justify-content: space-between;
168 padding: 0.5rem 0.75rem;
169 border-bottom: 1px solid var(--border);
170 background: color-mix(in srgb, var(--background) 80%, var(--card) 20%);
171 }
172
173 .header-label {
174 font-size: 0.6rem;
175 font-weight: 600;
176 color: var(--muted-foreground);
177 letter-spacing: 0.12em;
178 }
179
180 .add-folder-btn {
181 width: 20px;
182 height: 20px;
183 display: flex;
184 align-items: center;
185 justify-content: center;
186 font-size: 0.9rem;
187 font-weight: 500;
188 border-width: 1px;
189 border-style: solid;
190 border-color: var(--border);
191 background: transparent;
192 color: var(--muted-foreground);
193 cursor: pointer;
194 transition: all 100ms ease;
195 }
196
197 .add-folder-btn:hover {
198 color: var(--foreground);
199 border-color: var(--foreground);
200 }
201
202 .add-folder-btn.cancel {
203 color: var(--muted-foreground);
204 }
205
206 .add-folder-btn.cancel:hover {
207 color: var(--destructive);
208 border-color: var(--destructive);
209 }
210
211 .create-input-row {
212 padding: 0.4rem 0.5rem;
213 border-bottom: 1px solid var(--border);
214 background: color-mix(in srgb, var(--primary, #4ade80) 8%, var(--background) 92%);
215 }
216
217 .create-input {
218 width: 100%;
219 padding: 0.35rem 0.5rem;
220 font-size: 0.7rem;
221 font-family: inherit;
222 color: var(--foreground);
223 background: var(--background);
224 border-width: 1px;
225 border-style: solid;
226 border-color: var(--primary, #4ade80);
227 outline: none;
228 }
229
230 .create-input::placeholder {
231 color: var(--muted-foreground);
232 }
233
234 .folder-items {
235 display: flex;
236 flex-direction: column;
237 }
238
239 .folder-item {
240 display: flex;
241 align-items: center;
242 gap: 0.5rem;
243 padding: 0.5rem 0.75rem;
244 text-align: left;
245 background: transparent;
246 border: none;
247 border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
248 cursor: pointer;
249 transition: background-color 100ms ease;
250 }
251
252 .folder-item:last-child {
253 border-bottom: none;
254 }
255
256 .folder-item:hover {
257 background: color-mix(in srgb, var(--card) 30%, transparent);
258 }
259
260 .folder-item.active {
261 background: color-mix(in srgb, var(--card) 50%, transparent);
262 }
263
264 .folder-item.active::before {
265 content: '';
266 position: absolute;
267 left: 0;
268 top: 0;
269 bottom: 0;
270 width: 2px;
271 background: var(--primary, #4ade80);
272 }
273
274 .folder-item {
275 position: relative;
276 }
277
278 .folder-icon {
279 font-size: 0.8rem;
280 width: 1.2rem;
281 text-align: center;
282 color: var(--muted-foreground);
283 }
284
285 .folder-item.active .folder-icon {
286 color: var(--foreground);
287 }
288
289 .folder-name {
290 flex: 1;
291 font-size: 0.72rem;
292 color: var(--foreground);
293 white-space: nowrap;
294 overflow: hidden;
295 text-overflow: ellipsis;
296 }
297
298 .folder-item.unfiled .folder-name {
299 font-style: italic;
300 color: var(--muted-foreground);
301 }
302
303 .folder-count {
304 font-size: 0.6rem;
305 padding: 0.1rem 0.35rem;
306 border-width: 1px;
307 border-style: solid;
308 border-color: var(--border);
309 background: var(--background);
310 color: var(--muted-foreground);
311 }
312</style>