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>