v0.23.2 bfd5eb4

HistoryCleanup.svelte RawHistoryFolder

1<script lang="ts">
2  import type { HistoryCleanupRange } from '../types';
3
4  interface Props {
5    onCleanup: (range: HistoryCleanupRange) => void;
6    onClose: () => void;
7  }
8
9  let { onCleanup, onClose }: Props = $props();
10
11  type CleanupOption = {
12    range: HistoryCleanupRange;
13    label: string;
14    shortcut: string;
15    description: string;
16    destructive?: boolean;
17  };
18
19  const options: CleanupOption[] = [
20    { range: 'hour', label: 'LAST HOUR', shortcut: 'D h', description: 'Delete entries from the past 60 minutes' },
21    { range: 'day', label: 'LAST DAY', shortcut: 'D d', description: 'Delete entries from the past 24 hours' },
22    { range: 'week', label: 'LAST WEEK', shortcut: 'D w', description: 'Delete entries from the past 7 days' },
23    { range: 'month', label: 'LAST MONTH', shortcut: 'D m', description: 'Delete entries from the past 30 days' },
24    { range: 'all', label: 'CLEAR ALL', shortcut: 'D D', description: 'Permanently delete all browsing history', destructive: true },
25  ];
26
27  let selectedIndex = $state(0);
28  let confirming = $state(false);
29
30  const handleKeyDown = (e: KeyboardEvent) => {
31    switch (e.key) {
32      case 'Escape':
33        e.preventDefault();
34        if (confirming) {
35          confirming = false;
36        } else {
37          onClose();
38        }
39        break;
40      case 'ArrowDown':
41      case 'j':
42        e.preventDefault();
43        selectedIndex = Math.min(selectedIndex + 1, options.length - 1);
44        break;
45      case 'ArrowUp':
46      case 'k':
47        e.preventDefault();
48        selectedIndex = Math.max(selectedIndex - 1, 0);
49        break;
50      case 'Enter':
51        e.preventDefault();
52        handleSelect(options[selectedIndex]!);
53        break;
54    }
55  };
56
57  const handleSelect = (option: CleanupOption) => {
58    if (option.destructive && !confirming) {
59      confirming = true;
60      return;
61    }
62    onCleanup(option.range);
63    onClose();
64  };
65
66  const handleOverlayClick = (e: Event) => {
67    if (e.target === e.currentTarget) {
68      onClose();
69    }
70  };
71</script>
72
73<svelte:window onkeydown={handleKeyDown} />
74
75<div
76  class="cleanup-overlay"
77  onclick={handleOverlayClick}
78  onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
79  role="button"
80  tabindex="0"
81  aria-label="Close cleanup modal"
82>
83  <div class="cleanup-modal" role="presentation">
84    <div class="modal-header">
85      <span class="modal-icon"></span>
86      <span class="modal-title">CLEAR HISTORY</span>
87      <button class="close-btn" type="button" onclick={onClose}>
88        <kbd>Esc</kbd>
89      </button>
90    </div>
91
92    {#if confirming}
93      <div class="confirm-panel">
94        <div class="confirm-icon"></div>
95        <p class="confirm-message">
96          This will permanently delete <strong>ALL</strong> browsing history.
97          This action cannot be undone.
98        </p>
99        <div class="confirm-actions">
100          <button
101            class="confirm-btn cancel"
102            type="button"
103            onclick={() => confirming = false}
104          >
105            CANCEL
106            <kbd>Esc</kbd>
107          </button>
108          <button
109            class="confirm-btn destructive"
110            type="button"
111            onclick={() => { onCleanup('all'); onClose(); }}
112          >
113            DELETE ALL
114            <kbd>Enter</kbd>
115          </button>
116        </div>
117      </div>
118    {:else}
119      <div class="cleanup-options">
120        {#each options as option, i (option.range)}
121          <button
122            class="cleanup-option"
123            class:selected={selectedIndex === i}
124            class:destructive={option.destructive}
125            type="button"
126            onclick={() => handleSelect(option)}
127            onmouseenter={() => selectedIndex = i}
128          >
129            <div class="option-main">
130              <span class="option-label">{option.label}</span>
131              <span class="option-desc">{option.description}</span>
132            </div>
133            <kbd class="option-shortcut">{option.shortcut}</kbd>
134          </button>
135        {/each}
136      </div>
137    {/if}
138
139    <div class="modal-footer">
140      <span class="hint"><kbd>j</kbd><kbd>k</kbd> navigate</span>
141      <span class="hint"><kbd>Enter</kbd> select</span>
142    </div>
143  </div>
144</div>
145
146<style>
147  .cleanup-overlay {
148    position: fixed;
149    inset: 0;
150    z-index: 100;
151    display: flex;
152    align-items: center;
153    justify-content: center;
154    background: rgb(0 0 0 / 0.75);
155    backdrop-filter: blur(4px);
156    animation: fade-in 150ms ease;
157  }
158
159  @keyframes fade-in {
160    from { opacity: 0; }
161  }
162
163  .cleanup-modal {
164    width: 100%;
165    max-width: 420px;
166    margin: 1rem;
167    background: var(--card);
168    border-width: 1px;
169    border-style: solid;
170    border-color: var(--border);
171    box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6);
172    animation: modal-in 150ms ease;
173  }
174
175  @keyframes modal-in {
176    from {
177      opacity: 0;
178      transform: scale(0.96) translateY(-8px);
179    }
180  }
181
182  .modal-header {
183    display: flex;
184    align-items: center;
185    gap: 0.6rem;
186    padding: 0.75rem 1rem;
187    border-bottom: 1px solid var(--border);
188    background: color-mix(in srgb, var(--background) 80%, transparent);
189  }
190
191  .modal-icon::before {
192    content: '';
193    font-size: 0.9rem;
194    color: var(--warning);
195  }
196
197  .modal-title {
198    flex: 1;
199    font-size: 0.72rem;
200    font-weight: 600;
201    letter-spacing: 0.1em;
202    color: var(--foreground);
203  }
204
205  .close-btn {
206    padding: 0.25rem 0.5rem;
207    background: transparent;
208    border-width: 1px;
209    border-style: solid;
210    border-color: var(--border);
211    cursor: pointer;
212  }
213
214  .close-btn kbd {
215    font-size: 0.6rem;
216    color: var(--muted-foreground);
217  }
218
219  .close-btn:hover {
220    border-color: var(--foreground);
221  }
222
223  .close-btn:hover kbd {
224    color: var(--foreground);
225  }
226
227  .cleanup-options {
228    display: flex;
229    flex-direction: column;
230  }
231
232  .cleanup-option {
233    display: flex;
234    align-items: center;
235    justify-content: space-between;
236    gap: 1rem;
237    padding: 0.85rem 1rem;
238    text-align: left;
239    background: transparent;
240    border: none;
241    border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
242    cursor: pointer;
243    transition: background-color 100ms ease;
244  }
245
246  .cleanup-option:last-child {
247    border-bottom: none;
248  }
249
250  .cleanup-option:hover,
251  .cleanup-option.selected {
252    background: color-mix(in srgb, var(--card) 80%, var(--background) 20%);
253  }
254
255  .cleanup-option.selected {
256    outline: 1px solid var(--primary, #4ade80);
257    outline-offset: -1px;
258  }
259
260  .cleanup-option.destructive .option-label {
261    color: var(--destructive);
262  }
263
264  .option-main {
265    display: flex;
266    flex-direction: column;
267    gap: 0.2rem;
268  }
269
270  .option-label {
271    font-size: 0.75rem;
272    font-weight: 600;
273    letter-spacing: 0.08em;
274    color: var(--foreground);
275  }
276
277  .option-desc {
278    font-size: 0.68rem;
279    color: var(--muted-foreground);
280    letter-spacing: 0.04em;
281  }
282
283  .option-shortcut {
284    padding: 0.2rem 0.45rem;
285    font-size: 0.58rem;
286    letter-spacing: 0.05em;
287    border-width: 1px;
288    border-style: solid;
289    border-color: var(--border);
290    background: var(--background);
291    color: var(--muted-foreground);
292  }
293
294  .confirm-panel {
295    padding: 1.5rem 1rem;
296    display: flex;
297    flex-direction: column;
298    align-items: center;
299    gap: 1rem;
300    text-align: center;
301  }
302
303  .confirm-icon::before {
304    content: '';
305    font-size: 2rem;
306    color: var(--destructive);
307  }
308
309  .confirm-message {
310    margin: 0;
311    font-size: 0.8rem;
312    line-height: 1.5;
313    color: var(--foreground);
314  }
315
316  .confirm-message strong {
317    color: var(--destructive);
318  }
319
320  .confirm-actions {
321    display: flex;
322    gap: 0.5rem;
323    width: 100%;
324    margin-top: 0.5rem;
325  }
326
327  .confirm-btn {
328    flex: 1;
329    display: flex;
330    align-items: center;
331    justify-content: center;
332    gap: 0.5rem;
333    padding: 0.6rem 1rem;
334    font-size: 0.7rem;
335    font-weight: 500;
336    letter-spacing: 0.08em;
337    border-width: 1px;
338    border-style: solid;
339    border-color: var(--border);
340    cursor: pointer;
341    transition: all 100ms ease;
342  }
343
344  .confirm-btn kbd {
345    font-size: 0.55rem;
346    padding: 0.1rem 0.3rem;
347    border: 1px solid;
348    opacity: 0.7;
349  }
350
351  .confirm-btn.cancel {
352    background: transparent;
353    color: var(--muted-foreground);
354  }
355
356  .confirm-btn.cancel:hover {
357    color: var(--foreground);
358    border-color: var(--foreground);
359  }
360
361  .confirm-btn.destructive {
362    background: var(--destructive);
363    color: var(--destructive-foreground);
364    border-color: color-mix(in srgb, var(--destructive) 80%, black 20%);
365  }
366
367  .confirm-btn.destructive:hover {
368    background: color-mix(in srgb, var(--destructive) 85%, white 15%);
369  }
370
371  .modal-footer {
372    display: flex;
373    align-items: center;
374    justify-content: center;
375    gap: 1rem;
376    padding: 0.6rem 1rem;
377    border-top: 1px solid var(--border);
378    background: color-mix(in srgb, var(--background) 60%, transparent);
379  }
380
381  .hint {
382    font-size: 0.6rem;
383    color: var(--muted-foreground);
384    letter-spacing: 0.05em;
385  }
386
387  .hint kbd {
388    font-size: 0.55rem;
389    padding: 0.1rem 0.25rem;
390    margin-right: 0.25rem;
391    border-width: 1px;
392    border-style: solid;
393    border-color: var(--border);
394    background: var(--background);
395  }
396</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