FavoritesPanel.svelte RawHistoryFolder
1<script lang="ts">
2 import { homepageState } from '../state.svelte';
3 import {
4 navigateTo,
5 setFavoriteShortcut,
6 setFavoriteFolder,
7 assignTag,
8 removeTag,
9 createFolder,
10 createTag
11 } from '../messaging';
12 import type { Favorite } from '../types';
13
14 import { FolderList, TagCloud, FavoriteGrid, FavoriteEditor } from '../favorites';
15
16 let editorOpen = $state(false);
17 let editingFavorite = $state<Favorite | null>(null);
18
19 // Get filtered favorites based on folder and tag selection
20 const displayedFavorites = $derived(() => {
21 let result = homepageState.favorites;
22
23 // Filter by folder
24 const folderId = homepageState.selectedFolderId;
25 if (folderId !== null) {
26 if (folderId === -1) {
27 // Show unfiled
28 result = result.filter(f => f.folder_id === null);
29 } else {
30 result = result.filter(f => f.folder_id === folderId);
31 }
32 }
33
34 // Filter by tags
35 const tagIds = homepageState.selectedTagIds;
36 if (tagIds.length > 0) {
37 result = result.filter(f =>
38 f.tags?.some(t => tagIds.includes(t.id))
39 );
40 }
41
42 return result;
43 });
44
45 const handleSelectFavorite = (fav: Favorite) => {
46 navigateTo(fav.url);
47 };
48
49 const handleEditFavorite = (fav: Favorite) => {
50 editingFavorite = fav;
51 editorOpen = true;
52 };
53
54 const handleSaveFavorite = async (updates: Partial<Favorite>) => {
55 if (!updates.id) return;
56
57 // Update shortcut key
58 if (updates.shortcut_key !== undefined) {
59 await setFavoriteShortcut(updates.id, updates.shortcut_key);
60 }
61
62 // Update folder
63 if (updates.folder_id !== undefined) {
64 await setFavoriteFolder(updates.id, updates.folder_id);
65 }
66
67 // Title update would need a separate handler - for now just update local state
68 if (updates.title !== undefined && editingFavorite) {
69 homepageState.updateFavorite({
70 ...editingFavorite,
71 ...updates
72 });
73 }
74 };
75
76 const handleDeleteFavorite = (id: number) => {
77 // This would call a delete API - for now just update local state
78 homepageState.deleteFavorite(id);
79 };
80
81 const handleAssignTag = async (favoriteId: number, tagId: number) => {
82 await assignTag(favoriteId, tagId);
83 };
84
85 const handleRemoveTag = async (favoriteId: number, tagId: number) => {
86 await removeTag(favoriteId, tagId);
87 };
88
89 const handleSelectFolder = (id: number | null) => {
90 homepageState.selectFolder(id);
91 };
92
93 const handleToggleTag = (id: number) => {
94 homepageState.toggleTag(id);
95 };
96
97 const handleClearTagSelection = () => {
98 homepageState.clearTagSelection();
99 };
100
101 const handleCreateFolder = async (name: string) => {
102 await createFolder({ name });
103 };
104
105 const handleCreateTag = async (name: string, color: string) => {
106 await createTag({ name, color });
107 };
108
109 const closeEditor = () => {
110 editorOpen = false;
111 editingFavorite = null;
112 };
113</script>
114
115<div class="favorites-panel">
116 <div class="panel-sidebar">
117 <FolderList
118 folders={homepageState.folders}
119 favorites={homepageState.favorites}
120 selectedFolderId={homepageState.selectedFolderId}
121 onSelectFolder={handleSelectFolder}
122 onCreateFolder={handleCreateFolder}
123 />
124
125 <TagCloud
126 tags={homepageState.tags}
127 selectedTagIds={homepageState.selectedTagIds}
128 onToggleTag={handleToggleTag}
129 onClearSelection={handleClearTagSelection}
130 onCreateTag={handleCreateTag}
131 />
132 </div>
133
134 <div class="panel-main">
135 <div class="panel-header">
136 <span class="header-title">
137 {#if homepageState.selectedFolderId === null}
138 ALL FAVORITES
139 {:else if homepageState.selectedFolderId === -1}
140 UNFILED
141 {:else}
142 {homepageState.folders.find(f => f.id === homepageState.selectedFolderId)?.name ?? 'FOLDER'}
143 {/if}
144 </span>
145 <span class="header-count">{displayedFavorites().length}</span>
146 </div>
147
148 <div class="panel-content">
149 {#if homepageState.favoritesLoading}
150 <div class="loading-state">
151 <span class="loading-spinner"></span>
152 <span class="loading-text">LOADING FAVORITES...</span>
153 </div>
154 {:else}
155 <FavoriteGrid
156 favorites={displayedFavorites()}
157 onSelectFavorite={handleSelectFavorite}
158 onEditFavorite={handleEditFavorite}
159 />
160 {/if}
161 </div>
162 </div>
163
164 {#if editorOpen && editingFavorite}
165 <FavoriteEditor
166 favorite={editingFavorite}
167 folders={homepageState.folders}
168 tags={homepageState.tags}
169 onSave={handleSaveFavorite}
170 onDelete={handleDeleteFavorite}
171 onClose={closeEditor}
172 onAssignTag={handleAssignTag}
173 onRemoveTag={handleRemoveTag}
174 />
175 {/if}
176</div>
177
178<style>
179 .favorites-panel {
180 display: grid;
181 grid-template-columns: 200px 1fr;
182 gap: 0.75rem;
183 padding: 0.5rem 1rem;
184 }
185
186 .panel-sidebar {
187 display: flex;
188 flex-direction: column;
189 gap: 0.5rem;
190 }
191
192 .panel-main {
193 display: flex;
194 flex-direction: column;
195 min-height: 0;
196 border-width: 1px;
197 border-style: solid;
198 border-color: var(--border);
199 background: color-mix(in srgb, var(--background) 95%, var(--card) 5%);
200 }
201
202 .panel-header {
203 display: flex;
204 align-items: center;
205 justify-content: space-between;
206 padding: 0.6rem 0.85rem;
207 border-bottom: 1px solid var(--border);
208 background: color-mix(in srgb, var(--background) 80%, var(--card) 20%);
209 }
210
211 .header-title {
212 font-size: 0.68rem;
213 font-weight: 600;
214 letter-spacing: 0.12em;
215 color: var(--foreground);
216 }
217
218 .header-count {
219 font-size: 0.6rem;
220 padding: 0.15rem 0.4rem;
221 border-width: 1px;
222 border-style: solid;
223 border-color: var(--border);
224 background: var(--background);
225 color: var(--muted-foreground);
226 }
227
228 .panel-content {
229 padding: 0.75rem;
230 }
231
232 .loading-state {
233 flex: 1;
234 display: flex;
235 flex-direction: column;
236 align-items: center;
237 justify-content: center;
238 gap: 0.75rem;
239 padding: 2rem;
240 color: var(--muted-foreground);
241 }
242
243 .loading-spinner {
244 width: 24px;
245 height: 24px;
246 border-width: 2px;
247 border-style: solid;
248 border-color: var(--border);
249 border-top-color: var(--primary, #4ade80);
250 border-radius: 50%;
251 animation: spin 0.8s linear infinite;
252 }
253
254 @keyframes spin {
255 to { transform: rotate(360deg); }
256 }
257
258 .loading-text {
259 font-size: 0.68rem;
260 letter-spacing: 0.12em;
261 animation: pulse 1.5s ease-in-out infinite;
262 }
263
264 @keyframes pulse {
265 0%, 100% { opacity: 0.5; }
266 50% { opacity: 1; }
267 }
268
269 /* Responsive: stack on smaller screens */
270 @media (max-width: 768px) {
271 .favorites-panel {
272 grid-template-columns: 1fr;
273 grid-template-rows: auto 1fr;
274 }
275
276 .panel-sidebar {
277 flex-direction: row;
278 gap: 0.5rem;
279 overflow-x: auto;
280 }
281
282 .panel-sidebar > :global(*) {
283 flex-shrink: 0;
284 min-width: 180px;
285 }
286 }
287</style>