v0.26.2 3cd9d2e

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