v0.22.0 a85da87

StatusBar.svelte RawHistoryFolder

1<script lang="ts">
2  import { homepageState } from '../state.svelte';
3  import { History, Star, BarChart3, Terminal, Search, Folder, Tags, Command } from '@lucide/svelte';
4
5  // Derived status indicators
6  const panelLabel = $derived({
7    history: 'HISTORY',
8    favorites: 'FAVORITES',
9    analytics: 'ANALYTICS',
10  }[homepageState.activePanel]);
11
12  const panelIcon = $derived({
13    history: History,
14    favorites: Star,
15    analytics: BarChart3,
16  }[homepageState.activePanel]);
17
18  // Loading states
19  const isLoading = $derived(
20    homepageState.historyLoading ||
21    homepageState.favoritesLoading ||
22    homepageState.analyticsLoading
23  );
24
25  // Current list stats
26  const listStats = $derived.by(() => {
27    switch (homepageState.activePanel) {
28      case 'history':
29        if (homepageState.historySearchQuery) {
30          return {
31            count: homepageState.historySearchResults.length,
32            label: 'results',
33            filtered: true,
34          };
35        }
36        return {
37          count: homepageState.history.length,
38          label: 'entries',
39          filtered: false,
40        };
41      case 'favorites':
42        const filtered = homepageState.filteredFavorites;
43        const total = homepageState.favorites.length;
44        return {
45          count: filtered.length,
46          label: filtered.length === total ? 'items' : `of ${total}`,
47          filtered: filtered.length !== total,
48        };
49      case 'analytics':
50        return {
51          count: homepageState.domainStats.length,
52          label: 'domains',
53          filtered: false,
54        };
55      default:
56        return { count: 0, label: 'items', filtered: false };
57    }
58  });
59
60  // Position in list
61  const positionInfo = $derived.by(() => {
62    const length = homepageState.currentListLength;
63    if (length === 0) return null;
64
65    const pos = homepageState.focusedIndex + 1;
66    return { current: pos, total: length };
67  });
68
69  // Mode indicator
70  const modeIndicator = $derived.by(() => {
71    if (homepageState.commandPaletteOpen) {
72      return { icon: Terminal, label: 'COMMAND', color: 'accent' };
73    }
74    if (homepageState.historySearchQuery) {
75      return { icon: Search, label: 'SEARCH', color: 'warning' };
76    }
77    if (homepageState.selectedFolderId !== null) {
78      const folder = homepageState.folders.find(f => f.id === homepageState.selectedFolderId);
79      return { icon: Folder, label: folder?.name || 'FOLDER', color: 'info' };
80    }
81    if (homepageState.selectedTagIds.length > 0) {
82      return { icon: Tags, label: `${homepageState.selectedTagIds.length} TAGS`, color: 'info' };
83    }
84    return null;
85  });
86</script>
87
88<footer class="status-bar">
89  <!-- Left Section: Mode & Panel -->
90  <div class="status-left">
91    <!-- Pending Key Prefix Indicator -->
92    {#if homepageState.pendingKeyPrefix}
93      <div class="status-chip prefix-chip" data-color="warning">
94        <span class="chip-icon">
95          <Command size={12} strokeWidth={2} />
96        </span>
97        <kbd class="prefix-key">{homepageState.pendingKeyPrefix}</kbd>
98        <span class="prefix-waiting">...</span>
99      </div>
100    {/if}
101
102    <!-- Mode Indicator -->
103    {#if modeIndicator}
104      {@const ModeIcon = modeIndicator.icon}
105      <div class="status-chip mode-chip" data-color={modeIndicator.color}>
106        <span class="chip-icon">
107          <ModeIcon size={12} strokeWidth={2} />
108        </span>
109        <span class="chip-text">{modeIndicator.label}</span>
110      </div>
111    {/if}
112
113    <!-- Panel Indicator -->
114    <div class="status-chip panel-chip">
115      <span class="chip-icon">
116        {#if panelIcon}
117          {@const PanelIcon = panelIcon}
118          <PanelIcon size={12} strokeWidth={2} />
119        {/if}
120      </span>
121      <span class="chip-text">{panelLabel}</span>
122    </div>
123
124    <!-- Loading Indicator -->
125    {#if isLoading}
126      <div class="status-chip loading-chip">
127        <span class="loading-spinner"></span>
128        <span class="chip-text">SYNC</span>
129      </div>
130    {/if}
131  </div>
132
133  <!-- Center Section: Stats -->
134  <div class="status-center">
135    <div class="stats-group">
136      <span class="stats-count" class:filtered={listStats.filtered}>
137        {listStats.count}
138      </span>
139      <span class="stats-label">{listStats.label}</span>
140    </div>
141
142    {#if positionInfo}
143      <div class="position-group">
144        <span class="position-current">{positionInfo.current}</span>
145        <span class="position-sep">/</span>
146        <span class="position-total">{positionInfo.total}</span>
147      </div>
148    {/if}
149  </div>
150
151  <!-- Right Section: Quick Hints -->
152  <div class="status-right">
153    <div class="hint-item">
154      <kbd>/</kbd>
155      <span>cmd</span>
156    </div>
157    <div class="hint-item">
158      <kbd>j</kbd><kbd>k</kbd>
159      <span>nav</span>
160    </div>
161    <div class="hint-item">
162      <kbd>?</kbd>
163      <span>help</span>
164    </div>
165  </div>
166</footer>
167
168<style>
169  .status-bar {
170    display: flex;
171    align-items: center;
172    justify-content: space-between;
173    gap: 1rem;
174    padding: 0.5rem 0.85rem;
175    font-size: 0.68rem;
176    font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace;
177    letter-spacing: 0.05em;
178    border-top: 1px solid var(--border);
179    background: color-mix(in srgb, var(--background) 92%, var(--card) 8%);
180    color: var(--muted-foreground);
181  }
182
183  /* Sections */
184  .status-left,
185  .status-center,
186  .status-right {
187    display: flex;
188    align-items: center;
189    gap: 0.6rem;
190  }
191
192  .status-left {
193    flex: 1;
194    justify-content: flex-start;
195  }
196
197  .status-center {
198    flex: 0 0 auto;
199    gap: 1rem;
200  }
201
202  .status-right {
203    flex: 1;
204    justify-content: flex-end;
205    gap: 0.85rem;
206  }
207
208  /* Status Chips */
209  .status-chip {
210    display: inline-flex;
211    align-items: center;
212    gap: 0.35rem;
213    padding: 0.25rem 0.5rem;
214    background: color-mix(in srgb, var(--card) 60%, transparent);
215    border-width: 1px;
216    border-style: solid;
217    border-color: var(--border);
218  }
219
220  .chip-icon {
221    display: flex;
222    align-items: center;
223  }
224
225  .chip-text {
226    font-weight: 500;
227    text-transform: uppercase;
228  }
229
230  /* Mode chip colors */
231  .mode-chip[data-color="accent"] {
232    background: color-mix(in srgb, var(--primary, #4ade80) 15%, transparent);
233    border-color: color-mix(in srgb, var(--primary, #4ade80) 40%, var(--border) 60%);
234    color: var(--primary, #4ade80);
235  }
236
237  .mode-chip[data-color="warning"] {
238    background: color-mix(in srgb, var(--warning) 15%, transparent);
239    border-color: color-mix(in srgb, var(--warning) 40%, var(--border) 60%);
240    color: var(--warning);
241  }
242
243  .mode-chip[data-color="info"] {
244    background: color-mix(in srgb, var(--info) 15%, transparent);
245    border-color: color-mix(in srgb, var(--info) 40%, var(--border) 60%);
246    color: var(--info);
247  }
248
249  /* Pending prefix indicator */
250  .prefix-chip {
251    animation: pulse-border 1s ease-in-out infinite;
252  }
253
254  .prefix-chip[data-color="warning"] {
255    background: color-mix(in srgb, var(--warning) 15%, transparent);
256    border-color: color-mix(in srgb, var(--warning) 50%, var(--border) 50%);
257    color: var(--warning);
258  }
259
260  .prefix-key {
261    font-size: 0.7rem;
262    font-weight: 700;
263    padding: 0.1rem 0.25rem;
264    background: color-mix(in srgb, var(--warning) 25%, transparent);
265    border: 1px solid color-mix(in srgb, var(--warning) 40%, var(--border) 60%);
266    font-family: ui-monospace, 'Fira Code', 'Cascadia Code', Menlo, Monaco, Consolas, monospace;
267  }
268
269  .prefix-waiting {
270    font-size: 0.65rem;
271    opacity: 0.8;
272    animation: blink 0.6s ease-in-out infinite;
273  }
274
275  @keyframes pulse-border {
276    0%, 100% { border-color: color-mix(in srgb, var(--warning) 50%, var(--border) 50%); }
277    50% { border-color: var(--warning); }
278  }
279
280  @keyframes blink {
281    0%, 100% { opacity: 0.4; }
282    50% { opacity: 1; }
283  }
284
285  .panel-chip {
286    color: var(--foreground);
287  }
288
289  .panel-chip .chip-icon {
290    color: var(--primary, #4ade80);
291  }
292
293  /* Loading */
294  .loading-chip {
295    color: var(--muted-foreground);
296  }
297
298  .loading-spinner {
299    width: 0.7rem;
300    height: 0.7rem;
301    border: 1.5px solid var(--border);
302    border-top-color: var(--primary, #4ade80);
303    border-radius: 50%;
304    animation: spin 0.8s linear infinite;
305  }
306
307  @keyframes spin {
308    to { transform: rotate(360deg); }
309  }
310
311  /* Stats */
312  .stats-group {
313    display: flex;
314    align-items: baseline;
315    gap: 0.35rem;
316  }
317
318  .stats-count {
319    font-weight: 600;
320    color: var(--foreground);
321    font-variant-numeric: tabular-nums;
322  }
323
324  .stats-count.filtered {
325    color: var(--primary, #4ade80);
326  }
327
328  .stats-label {
329    color: var(--muted-foreground);
330  }
331
332  .position-group {
333    display: flex;
334    align-items: center;
335    gap: 0.15rem;
336    padding: 0.2rem 0.45rem;
337    background: var(--background);
338    border-width: 1px;
339    border-style: solid;
340    border-color: var(--border);
341    font-variant-numeric: tabular-nums;
342  }
343
344  .position-current {
345    font-weight: 600;
346    color: var(--foreground);
347  }
348
349  .position-sep {
350    color: var(--border);
351  }
352
353  .position-total {
354    color: var(--muted-foreground);
355  }
356
357  /* Hints */
358  .hint-item {
359    display: flex;
360    align-items: center;
361    gap: 0.25rem;
362    color: var(--muted-foreground);
363  }
364
365  .hint-item kbd {
366    padding: 0.1rem 0.3rem;
367    font-size: 0.6rem;
368    background: var(--background);
369    border-width: 1px;
370    border-style: solid;
371    border-color: var(--border);
372    color: var(--foreground);
373  }
374
375  .hint-item span {
376    font-size: 0.62rem;
377    text-transform: uppercase;
378    letter-spacing: 0.08em;
379  }
380
381  /* Responsive */
382  @media (max-width: 640px) {
383    .status-bar {
384      flex-wrap: wrap;
385      gap: 0.5rem;
386    }
387
388    .status-left,
389    .status-right {
390      flex: 0 0 auto;
391    }
392
393    .status-center {
394      order: -1;
395      width: 100%;
396      justify-content: center;
397      padding-bottom: 0.35rem;
398      border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
399    }
400
401    .hint-item span {
402      display: none;
403    }
404  }
405
406  @media (max-width: 480px) {
407    .status-right {
408      display: none;
409    }
410
411    .status-left {
412      flex: 1;
413    }
414  }
415</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