v0.26.1 75cd777

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>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close