v0.28.0 3cb3770

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>

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