v0.27.2 5960dd8

FavoriteCard.svelte RawHistoryFolder

1<script lang="ts">
2  import type { Favorite } from '../types';
3
4  interface Props {
5    favorite: Favorite;
6    focused?: boolean;
7    onSelect?: (fav: Favorite) => void;
8    onEdit?: (fav: Favorite) => void;
9  }
10
11  let { favorite, focused = false, onSelect, onEdit }: Props = $props();
12
13  const getDomain = (url: string): string => {
14    try {
15      return new URL(url).hostname.replace('www.', '');
16    } catch {
17      return url;
18    }
19  };
20
21  const getFaviconUrl = (fav: Favorite): string => {
22    // Use provided favicon_url if available
23    if (fav.favicon_url) return fav.favicon_url;
24    // Handle internal dumb:// URLs - use embedded favicon
25    if (fav.url.startsWith('dumb://')) {
26      return 'favicon.png';
27    }
28    // Construct DuckDuckGo favicon URL from domain
29    const domain = getDomain(fav.url);
30    return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`;
31  };
32
33  const getDisplayTitle = (fav: Favorite): string => {
34    const trimmed = fav.title?.trim();
35    if (trimmed) return trimmed;
36    return getDomain(fav.url) || fav.url;
37  };
38
39  const handleSelect = () => {
40    onSelect?.(favorite);
41  };
42
43  const handleEdit = (e: Event) => {
44    e.stopPropagation();
45    onEdit?.(favorite);
46  };
47
48  const handleKeyDown = (e: KeyboardEvent) => {
49    switch (e.key) {
50      case 'Enter':
51        handleSelect();
52        break;
53      case 'e':
54        onEdit?.(favorite);
55        break;
56    }
57  };
58</script>
59
60<div
61  class="favorite-card"
62  class:focused
63  role="button"
64  tabindex="0"
65  data-favorite-id={favorite.id}
66  onclick={handleSelect}
67  onkeydown={handleKeyDown}
68>
69  <div class="card-favicon">
70    <img
71      src={getFaviconUrl(favorite)}
72      alt=""
73      class="favicon-img"
74      onerror={(e) => {
75        const target = e.target as HTMLImageElement;
76        target.style.display = 'none';
77        const fallback = target.nextElementSibling as HTMLElement;
78        if (fallback) fallback.style.display = 'flex';
79      }}
80    />
81    <div class="favicon-fallback" style="display: none;"></div>
82    {#if favorite.shortcut_key}
83      <div class="shortcut-badge">{favorite.shortcut_key}</div>
84    {/if}
85  </div>
86
87  <div class="card-content">
88    <span class="card-title">{getDisplayTitle(favorite)}</span>
89    <span class="card-domain">{getDomain(favorite.url)}</span>
90  </div>
91
92  {#if favorite.tags && favorite.tags.length > 0}
93    <div class="card-tags">
94      {#each favorite.tags.slice(0, 2) as tag (tag.id)}
95        <span class="tag-chip" style="--tag-color: {tag.color}">
96          {tag.name}
97        </span>
98      {/each}
99      {#if favorite.tags.length > 2}
100        <span class="tag-more">+{favorite.tags.length - 2}</span>
101      {/if}
102    </div>
103  {/if}
104
105  <div class="card-actions">
106    <button
107      class="action-btn"
108      type="button"
109      onclick={handleEdit}
110      title="Edit (e)"
111    >
112
113    </button>
114  </div>
115</div>
116
117<style>
118  .favorite-card {
119    display: grid;
120    grid-template-columns: auto 1fr auto auto;
121    align-items: center;
122    gap: 0.75rem;
123    padding: 0.65rem 0.85rem;
124    border-width: 1px;
125    border-style: solid;
126    border-color: var(--border);
127    background: color-mix(in srgb, var(--background) 92%, var(--card) 8%);
128    transition: all 100ms ease;
129    cursor: pointer;
130  }
131
132  .favorite-card:hover,
133  .favorite-card.focused {
134    background: color-mix(in srgb, var(--card) 40%, var(--background) 60%);
135    border-color: color-mix(in srgb, var(--border) 50%, var(--foreground) 50%);
136  }
137
138  .favorite-card.focused {
139    outline: 1px solid var(--primary, #4ade80);
140    outline-offset: -1px;
141  }
142
143  .card-favicon {
144    position: relative;
145    width: 32px;
146    height: 32px;
147    border-width: 1px;
148    border-style: solid;
149    border-color: var(--border);
150    background: var(--background);
151    display: flex;
152    align-items: center;
153    justify-content: center;
154    flex-shrink: 0;
155  }
156
157  .favicon-img {
158    width: 18px;
159    height: 18px;
160    object-fit: contain;
161  }
162
163  .favicon-fallback {
164    width: 18px;
165    height: 18px;
166    display: flex;
167    align-items: center;
168    justify-content: center;
169    color: var(--muted-foreground);
170  }
171
172  .favicon-fallback::before {
173    content: '';
174    font-size: 0.9rem;
175  }
176
177  .shortcut-badge {
178    position: absolute;
179    bottom: -4px;
180    right: -4px;
181    width: 14px;
182    height: 14px;
183    display: flex;
184    align-items: center;
185    justify-content: center;
186    font-size: 0.55rem;
187    font-weight: 700;
188    color: var(--background);
189    background: var(--primary, #4ade80);
190    border-width: 1px;
191    border-style: solid;
192    border-color: var(--background);
193  }
194
195  .card-content {
196    display: flex;
197    flex-direction: column;
198    gap: 0.15rem;
199    min-width: 0;
200  }
201
202  .card-title {
203    font-size: 0.8rem;
204    font-weight: 500;
205    color: var(--foreground);
206    white-space: nowrap;
207    overflow: hidden;
208    text-overflow: ellipsis;
209  }
210
211  .card-domain {
212    font-size: 0.65rem;
213    color: var(--muted-foreground);
214    letter-spacing: 0.06em;
215    white-space: nowrap;
216    overflow: hidden;
217    text-overflow: ellipsis;
218  }
219
220  .card-tags {
221    display: flex;
222    align-items: center;
223    gap: 0.3rem;
224  }
225
226  .tag-chip {
227    font-size: 0.55rem;
228    font-weight: 500;
229    padding: 0.15rem 0.4rem;
230    background: color-mix(in srgb, var(--tag-color) 20%, transparent);
231    border-width: 1px;
232    border-style: solid;
233    border-color: var(--tag-color);
234    color: var(--tag-color);
235    letter-spacing: 0.05em;
236  }
237
238  .tag-more {
239    font-size: 0.55rem;
240    color: var(--muted-foreground);
241    padding: 0.15rem 0.3rem;
242    border-width: 1px;
243    border-style: solid;
244    border-color: var(--border);
245  }
246
247  .card-actions {
248    display: flex;
249    gap: 0.3rem;
250    opacity: 0;
251    transition: opacity 100ms ease;
252  }
253
254  .favorite-card:hover .card-actions,
255  .favorite-card.focused .card-actions {
256    opacity: 1;
257  }
258
259  .action-btn {
260    width: 24px;
261    height: 24px;
262    display: flex;
263    align-items: center;
264    justify-content: center;
265    font-size: 0.8rem;
266    border-width: 1px;
267    border-style: solid;
268    border-color: var(--border);
269    background: transparent;
270    color: var(--muted-foreground);
271    cursor: pointer;
272    transition: all 100ms ease;
273  }
274
275  .action-btn:hover {
276    color: var(--foreground);
277    border-color: var(--foreground);
278    background: color-mix(in srgb, var(--card) 50%, transparent);
279  }
280</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