v0.27.1 c78ce83

HistoryPanel.svelte RawHistoryFolder

1<script lang="ts">
2  import { homepageState } from '../state.svelte';
3  import {
4    fetchHistoryTimeline,
5    deleteHistoryEntry,
6    deleteHistoryByRange,
7    clearAllHistory,
8    fetchDomainStats,
9    navigateTo
10  } from '../messaging';
11  import type { HistoryEntry, HistoryCleanupRange } from '../types';
12
13  import { HistoryTimeline, HistorySearch, HistoryFilters, HistoryCleanup } from '../history';
14
15  let cleanupOpen = $state(false);
16  let selectedDomain = $state<string | null>(null);
17
18  // Filter entries by domain if selected
19  const displayedEntries = $derived(() => {
20    if (!selectedDomain) {
21      return homepageState.historySearchQuery
22        ? homepageState.historySearchResults
23        : homepageState.history;
24    }
25
26    const entries = homepageState.historySearchQuery
27      ? homepageState.historySearchResults
28      : homepageState.history;
29
30    return entries.filter(entry => {
31      try {
32        return new URL(entry.url).hostname === selectedDomain;
33      } catch {
34        return false;
35      }
36    });
37  });
38
39  // Recompute timeline from filtered entries
40  /* eslint-disable svelte/prefer-svelte-reactivity -- temporary Map/Date for data grouping, not reactive state */
41  const displayedTimeline = $derived(() => {
42    const entries = displayedEntries();
43    if (entries.length === 0) return [];
44
45    const groups = new Map<string, HistoryEntry[]>();
46    const today = new Date();
47    today.setHours(0, 0, 0, 0);
48    const yesterday = new Date(today);
49    yesterday.setDate(yesterday.getDate() - 1);
50
51    for (const entry of entries) {
52      const date = new Date(entry.last_visited);
53      const dateKey = date.toISOString().split('T')[0] ?? '';
54      if (!groups.has(dateKey)) {
55        groups.set(dateKey, []);
56      }
57      groups.get(dateKey)!.push(entry);
58    }
59
60    const result: { date: string; label: string; entries: HistoryEntry[] }[] = [];
61    for (const [dateKey, items] of groups) {
62      const date = new Date(dateKey);
63      let label: string;
64
65      if (date >= today) {
66        label = 'Today';
67      } else if (date >= yesterday) {
68        label = 'Yesterday';
69      } else {
70        label = date.toLocaleDateString(undefined, {
71          weekday: 'short',
72          month: 'short',
73          day: 'numeric',
74        });
75      }
76
77      result.push({ date: dateKey, label, entries: items });
78    }
79
80    return result.sort((a, b) => b.date.localeCompare(a.date));
81  });
82  /* eslint-enable svelte/prefer-svelte-reactivity */
83
84  const handleSelectEntry = (entry: HistoryEntry) => {
85    navigateTo(entry.url);
86  };
87
88  const handleDeleteEntry = async (entry: HistoryEntry) => {
89    await deleteHistoryEntry(entry.id);
90  };
91
92  const handleLoadMore = async () => {
93    if (homepageState.hasMoreHistory && !homepageState.historyLoading) {
94      await fetchHistoryTimeline(50, homepageState.historyOffset);
95    }
96  };
97
98  const handleCleanup = async (range: HistoryCleanupRange) => {
99    if (range === 'all') {
100      await clearAllHistory();
101    } else {
102      await deleteHistoryByRange(range);
103    }
104    cleanupOpen = false;
105  };
106
107  const handleSelectDomain = (domain: string | null) => {
108    selectedDomain = domain;
109    homepageState.setFocusedIndex(0);
110  };
111
112  // Fetch domain stats on mount
113  $effect(() => {
114    fetchDomainStats(10);
115  });
116</script>
117
118<div class="history-panel">
119  <div class="panel-toolbar">
120    <HistorySearch placeholder="Search history (FTS)..." autofocus={true} />
121  </div>
122
123  <HistoryFilters
124    domains={homepageState.domainStats}
125    selectedDomain={selectedDomain}
126    onSelectDomain={handleSelectDomain}
127    onOpenCleanup={() => cleanupOpen = true}
128  />
129
130  <div class="panel-content">
131    {#if homepageState.historyLoading && homepageState.history.length === 0}
132      <div class="loading-state">
133        <span class="loading-spinner"></span>
134        <span class="loading-text">LOADING HISTORY...</span>
135      </div>
136    {:else}
137      <HistoryTimeline
138        groups={displayedTimeline()}
139        onSelectEntry={handleSelectEntry}
140        onDeleteEntry={handleDeleteEntry}
141        onLoadMore={handleLoadMore}
142      />
143    {/if}
144  </div>
145
146  {#if cleanupOpen}
147    <HistoryCleanup
148      onCleanup={handleCleanup}
149      onClose={() => cleanupOpen = false}
150    />
151  {/if}
152</div>
153
154<style>
155  .history-panel {
156    display: flex;
157    flex-direction: column;
158    gap: 0.5rem;
159    padding: 0.5rem 1rem;
160  }
161
162  .panel-toolbar {
163    flex-shrink: 0;
164  }
165
166  .panel-content {
167    display: flex;
168    flex-direction: column;
169    border-width: 1px;
170    border-style: solid;
171    border-color: var(--border);
172    background: color-mix(in srgb, var(--background) 95%, var(--card) 5%);
173  }
174
175  .loading-state {
176    flex: 1;
177    display: flex;
178    flex-direction: column;
179    align-items: center;
180    justify-content: center;
181    gap: 0.75rem;
182    color: var(--muted-foreground);
183  }
184
185  .loading-spinner {
186    width: 24px;
187    height: 24px;
188    border-width: 2px;
189    border-style: solid;
190    border-color: var(--border);
191    border-top-color: var(--primary, #4ade80);
192    border-radius: 50%;
193    animation: spin 0.8s linear infinite;
194  }
195
196  @keyframes spin {
197    to { transform: rotate(360deg); }
198  }
199
200  .loading-text {
201    font-size: 0.68rem;
202    letter-spacing: 0.12em;
203    animation: pulse 1.5s ease-in-out infinite;
204  }
205
206  @keyframes pulse {
207    0%, 100% { opacity: 0.5; }
208    50% { opacity: 1; }
209  }
210</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