v0.23.2 bfd5eb4

KeyboardHints.svelte RawHistoryFolder

1<script lang="ts">
2  import { homepageState } from '../state.svelte';
3  import type { KeyboardHint } from '../keyboard';
4
5  // Toggle visibility with ?
6  let showHints = $state(false);
7  let hintTimeout: ReturnType<typeof setTimeout> | null = null;
8
9  // Panel-specific hints
10  const panelHints = $derived.by((): KeyboardHint[] => {
11    switch (homepageState.activePanel) {
12      case 'history':
13        return [
14          { keys: ['d', 'x'], description: 'Delete entry' },
15          { keys: ['D h'], description: 'Clear last hour' },
16          { keys: ['D d'], description: 'Clear last day' },
17          { keys: ['D w'], description: 'Clear last week' },
18          { keys: ['D D'], description: 'Clear all history' },
19          { keys: ['D @'], description: 'Clear domain' },
20          { keys: ['p'], description: 'Pin to favorites' },
21        ];
22      case 'favorites':
23        return [
24          { keys: ['e'], description: 'Edit favorite' },
25          { keys: ['t'], description: 'Manage tags' },
26          { keys: ['m'], description: 'Move to folder' },
27          { keys: ['1-9'], description: 'Set shortcut key' },
28          { keys: ['d'], description: 'Delete favorite' },
29        ];
30      case 'analytics':
31        return [
32          { keys: ['r'], description: 'Refresh stats' },
33          { keys: ['D @'], description: 'Clear domain' },
34        ];
35      default:
36        return [];
37    }
38  });
39
40  // Navigation hints (always shown)
41  const navHints: KeyboardHint[] = [
42    { keys: ['j', '↓'], description: 'Move down' },
43    { keys: ['k', '↑'], description: 'Move up' },
44    { keys: ['g g'], description: 'Go to first' },
45    { keys: ['G'], description: 'Go to last' },
46    { keys: ['Enter', 'o'], description: 'Open item' },
47    { keys: ['/'], description: 'Search / Command' },
48    { keys: ['Esc'], description: 'Close / Cancel' },
49  ];
50
51  // Panel switch hints
52  const panelSwitchHints: KeyboardHint[] = [
53    { keys: ['g h'], description: 'History panel' },
54    { keys: ['g f'], description: 'Favorites panel' },
55    { keys: ['g a'], description: 'Analytics panel' },
56  ];
57
58  // Quick access hints
59  const quickAccessHints: KeyboardHint[] = [
60    { keys: ['1-9'], description: 'Quick navigate to shortcut' },
61  ];
62
63  // Handle keyboard toggle
64  function handleKeydown(e: KeyboardEvent) {
65    if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) {
66      // Don't trigger if in input
67      if ((e.target as HTMLElement).tagName === 'INPUT') return;
68      // Don't trigger if command palette is open
69      if (homepageState.commandPaletteOpen) return;
70
71      e.preventDefault();
72      toggleHints();
73    }
74  }
75
76  function toggleHints() {
77    showHints = !showHints;
78
79    // Auto-hide after 10 seconds
80    if (showHints) {
81      if (hintTimeout) clearTimeout(hintTimeout);
82      hintTimeout = setTimeout(() => {
83        showHints = false;
84      }, 10000);
85    } else {
86      if (hintTimeout) {
87        clearTimeout(hintTimeout);
88        hintTimeout = null;
89      }
90    }
91  }
92
93  function closeHints() {
94    showHints = false;
95    if (hintTimeout) {
96      clearTimeout(hintTimeout);
97      hintTimeout = null;
98    }
99  }
100
101  // Cleanup on destroy
102  import { onDestroy } from 'svelte';
103  onDestroy(() => {
104    if (hintTimeout) clearTimeout(hintTimeout);
105  });
106</script>
107
108<svelte:document onkeydown={handleKeydown} />
109
110{#if showHints}
111  <div
112    class="hints-overlay"
113    onclick={closeHints}
114    onkeydown={(e) => { if (e.key === 'Escape' || e.key === 'Enter') closeHints(); }}
115    role="button"
116    tabindex="0"
117    aria-label="Close keyboard hints"
118  >
119    <div class="hints-panel" onclick={(e) => e.stopPropagation()} role="presentation">
120      <div class="hints-header">
121        <span class="hints-icon"></span>
122        <span class="hints-title">KEYBOARD SHORTCUTS</span>
123        <button class="hints-close" onclick={closeHints} type="button">
124          <span>×</span>
125        </button>
126      </div>
127
128      <div class="hints-body">
129        <!-- Navigation Section -->
130        <section class="hints-section">
131          <h3 class="section-title">
132            <span class="section-icon"></span>
133            Navigation
134          </h3>
135          <div class="hints-grid">
136            {#each navHints as hint (hint.description)}
137              <div class="hint-row">
138                <div class="hint-keys">
139                  {#each hint.keys as key, i}
140                    {#if i > 0}<span class="key-sep">/</span>{/if}
141                    <kbd>{key}</kbd>
142                  {/each}
143                </div>
144                <span class="hint-desc">{hint.description}</span>
145              </div>
146            {/each}
147          </div>
148        </section>
149
150        <!-- Panel Switch Section -->
151        <section class="hints-section">
152          <h3 class="section-title">
153            <span class="section-icon"></span>
154            Panels
155          </h3>
156          <div class="hints-grid">
157            {#each panelSwitchHints as hint (hint.description)}
158              <div class="hint-row">
159                <div class="hint-keys">
160                  {#each hint.keys as key, i}
161                    {#if i > 0}<span class="key-sep">/</span>{/if}
162                    <kbd>{key}</kbd>
163                  {/each}
164                </div>
165                <span class="hint-desc">{hint.description}</span>
166              </div>
167            {/each}
168          </div>
169        </section>
170
171        <!-- Current Panel Section -->
172        {#if panelHints.length > 0}
173          <section class="hints-section panel-specific">
174            <h3 class="section-title">
175              <span class="section-icon"></span>
176              {homepageState.activePanel.charAt(0).toUpperCase() + homepageState.activePanel.slice(1)} Actions
177            </h3>
178            <div class="hints-grid">
179              {#each panelHints as hint (hint.description)}
180                <div class="hint-row">
181                  <div class="hint-keys">
182                    {#each hint.keys as key, i}
183                      {#if i > 0}<span class="key-sep">/</span>{/if}
184                      <kbd>{key}</kbd>
185                    {/each}
186                  </div>
187                  <span class="hint-desc">{hint.description}</span>
188                </div>
189              {/each}
190            </div>
191          </section>
192        {/if}
193
194        <!-- Quick Access Section -->
195        <section class="hints-section">
196          <h3 class="section-title">
197            <span class="section-icon"></span>
198            Quick Access
199          </h3>
200          <div class="hints-grid">
201            {#each quickAccessHints as hint (hint.description)}
202              <div class="hint-row">
203                <div class="hint-keys">
204                  {#each hint.keys as key, i}
205                    {#if i > 0}<span class="key-sep">/</span>{/if}
206                    <kbd>{key}</kbd>
207                  {/each}
208                </div>
209                <span class="hint-desc">{hint.description}</span>
210              </div>
211            {/each}
212          </div>
213        </section>
214      </div>
215
216      <div class="hints-footer">
217        <span class="footer-hint">Press <kbd>?</kbd> to toggle hints</span>
218      </div>
219    </div>
220  </div>
221{/if}
222
223<style>
224  .hints-overlay {
225    position: fixed;
226    inset: 0;
227    z-index: 150;
228    display: flex;
229    align-items: center;
230    justify-content: center;
231    background: rgb(0 0 0 / 0.6);
232    backdrop-filter: blur(4px);
233    animation: fade-in 150ms ease;
234  }
235
236  @keyframes fade-in {
237    from { opacity: 0; }
238  }
239
240  .hints-panel {
241    width: 100%;
242    max-width: 540px;
243    max-height: 85vh;
244    margin: 1rem;
245    display: flex;
246    flex-direction: column;
247    background: var(--card);
248    border-width: 1px;
249    border-style: solid;
250    border-color: var(--border);
251    box-shadow: 0 24px 48px -12px rgb(0 0 0 / 0.6);
252    animation: panel-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
253    overflow: hidden;
254  }
255
256  @keyframes panel-in {
257    from {
258      opacity: 0;
259      transform: scale(0.95);
260    }
261  }
262
263  /* Header */
264  .hints-header {
265    display: flex;
266    align-items: center;
267    gap: 0.6rem;
268    padding: 0.85rem 1rem;
269    border-bottom: 1px solid var(--border);
270    background: color-mix(in srgb, var(--background) 80%, transparent);
271  }
272
273  .hints-icon {
274    font-size: 1rem;
275    color: var(--primary, #4ade80);
276  }
277
278  .hints-title {
279    flex: 1;
280    font-size: 0.75rem;
281    font-weight: 600;
282    letter-spacing: 0.1em;
283    color: var(--foreground);
284  }
285
286  .hints-close {
287    display: flex;
288    align-items: center;
289    justify-content: center;
290    width: 1.75rem;
291    height: 1.75rem;
292    font-size: 1.25rem;
293    color: var(--muted-foreground);
294    background: transparent;
295    border-width: 1px;
296    border-style: solid;
297    border-color: var(--border);
298    cursor: pointer;
299    transition: all 120ms ease;
300  }
301
302  .hints-close:hover {
303    color: var(--foreground);
304    border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%);
305  }
306
307  /* Body */
308  .hints-body {
309    flex: 1;
310    overflow-y: auto;
311    padding: 1rem;
312    display: flex;
313    flex-direction: column;
314    gap: 1.25rem;
315  }
316
317  .hints-section {
318    display: flex;
319    flex-direction: column;
320    gap: 0.6rem;
321  }
322
323  .hints-section.panel-specific {
324    padding: 0.75rem;
325    background: color-mix(in srgb, var(--primary, #4ade80) 8%, transparent);
326    border: 1px solid color-mix(in srgb, var(--primary, #4ade80) 25%, var(--border) 75%);
327  }
328
329  .section-title {
330    display: flex;
331    align-items: center;
332    gap: 0.5rem;
333    margin: 0;
334    font-size: 0.72rem;
335    font-weight: 600;
336    text-transform: uppercase;
337    letter-spacing: 0.1em;
338    color: var(--foreground);
339  }
340
341  .section-icon {
342    font-size: 0.85rem;
343    color: var(--muted-foreground);
344  }
345
346  .panel-specific .section-icon {
347    color: var(--primary, #4ade80);
348  }
349
350  .hints-grid {
351    display: grid;
352    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
353    gap: 0.4rem;
354  }
355
356  .hint-row {
357    display: flex;
358    align-items: center;
359    gap: 0.6rem;
360    padding: 0.4rem 0.6rem;
361    background: color-mix(in srgb, var(--background) 60%, transparent);
362    border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
363  }
364
365  .hint-keys {
366    display: flex;
367    align-items: center;
368    gap: 0.2rem;
369    flex-shrink: 0;
370  }
371
372  .hint-keys kbd {
373    padding: 0.2rem 0.4rem;
374    font-size: 0.68rem;
375    font-family: inherit;
376    color: var(--foreground);
377    background: var(--background);
378    border-width: 1px;
379    border-style: solid;
380    border-color: var(--border);
381    min-width: 1.4rem;
382    text-align: center;
383  }
384
385  .key-sep {
386    font-size: 0.6rem;
387    color: var(--muted-foreground);
388    opacity: 0.6;
389  }
390
391  .hint-desc {
392    font-size: 0.72rem;
393    color: var(--muted-foreground);
394    white-space: nowrap;
395    overflow: hidden;
396    text-overflow: ellipsis;
397  }
398
399  /* Footer */
400  .hints-footer {
401    display: flex;
402    justify-content: center;
403    padding: 0.65rem 1rem;
404    border-top: 1px solid var(--border);
405    background: color-mix(in srgb, var(--background) 70%, transparent);
406  }
407
408  .footer-hint {
409    font-size: 0.68rem;
410    color: var(--muted-foreground);
411    letter-spacing: 0.05em;
412  }
413
414  .footer-hint kbd {
415    padding: 0.15rem 0.35rem;
416    font-size: 0.62rem;
417    background: var(--background);
418    border-width: 1px;
419    border-style: solid;
420    border-color: var(--border);
421    margin: 0 0.25rem;
422  }
423
424  /* Scrollbar */
425  .hints-body::-webkit-scrollbar {
426    width: 6px;
427  }
428
429  .hints-body::-webkit-scrollbar-track {
430    background: transparent;
431  }
432
433  .hints-body::-webkit-scrollbar-thumb {
434    background: var(--border);
435  }
436
437  /* Responsive */
438  @media (max-width: 480px) {
439    .hints-grid {
440      grid-template-columns: 1fr;
441    }
442
443    .hints-panel {
444      max-height: 90vh;
445    }
446  }
447</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