1<script lang="ts">
2 import { onMount } from 'svelte';
3 import Footer from '$components/Footer.svelte';
4 // Icons disabled for investigation: removed @lucide/svelte imports
5
6 interface HistoryItem {
7 id: number;
8 url: string;
9 title: string;
10 favicon_url?: string;
11 visit_count: number;
12 last_visited: string;
13 created_at: string;
14 }
15
16 interface Shortcut {
17 key: string;
18 description: string;
19 action: () => void;
20 }
21
22 interface HistoryStats {
23 total_entries: number;
24 recent_count: number;
25 oldest_entry?: string;
26 newest_entry?: string;
27 }
28
29 let historyItems = $state<HistoryItem[]>([]);
30 let topVisited = $state<HistoryItem[]>([]);
31 let shortcuts = $state<Shortcut[]>([]);
32 let stats = $state<HistoryStats | null>(null);
33 let historyLoading = $state(true);
34 let topVisitedLoading = $state(true);
35 let shortcutsLoading = $state(true);
36 let statsLoading = $state(true);
37 let deletingIds = $state<number[]>([]);
38 let historyOffset = $state(0);
39 let hasMoreHistory = $state(true);
40 let loadingMoreHistory = $state(false);
41
42 // Function for checking if an item is being deleted
43 const isDeleting = (id: number) => deletingIds.includes(id);
44
45 // Initialize shortcuts (these are browser shortcuts, not dynamic from API)
46 const initializeShortcuts = () => {
47 shortcuts = [
48 // Browser shortcuts
49 {
50 key: 'Ctrl+L',
51 description: 'Focus address bar',
52 action: () => console.log('Ctrl+L triggered')
53 },
54 {
55 key: 'Ctrl+F',
56 description: 'Find in page',
57 action: () => console.log('Ctrl+F triggered')
58 },
59 {
60 key: 'Ctrl+Shift+C',
61 description: 'Copy URL',
62 action: () => console.log('Ctrl+Shift+C triggered')
63 },
64 {
65 key: 'Ctrl+R / F5',
66 description: 'Reload page',
67 action: () => window.location.reload()
68 },
69 {
70 key: 'Ctrl+Shift+R',
71 description: 'Hard reload',
72 action: () => console.log('Ctrl+Shift+R triggered')
73 },
74 {
75 key: 'F12',
76 description: 'DevTools',
77 action: () => console.log('F12 triggered')
78 },
79 {
80 key: 'Ctrl+← / Ctrl+→',
81 description: 'Navigate back/forward',
82 action: () => console.log('Ctrl+Arrow triggered')
83 },
84 {
85 key: 'Ctrl+0',
86 description: 'Reset zoom',
87 action: () => console.log('Ctrl+0 triggered')
88 },
89 {
90 key: 'Ctrl++ / Ctrl+-',
91 description: 'Zoom in/out',
92 action: () => console.log('Ctrl+Zoom triggered')
93 },
94 // Workspace pane mode
95 {
96 key: 'Ctrl+P',
97 description: 'Enter pane mode',
98 action: () => console.log('Ctrl+P triggered')
99 },
100 {
101 key: '→ / R (pane mode)',
102 description: 'Split pane right',
103 action: () => console.log('Split right triggered')
104 },
105 {
106 key: '← / L (pane mode)',
107 description: 'Split pane left',
108 action: () => console.log('Split left triggered')
109 },
110 {
111 key: '↑ / U (pane mode)',
112 description: 'Split pane up',
113 action: () => console.log('Split up triggered')
114 },
115 {
116 key: '↓ / D (pane mode)',
117 description: 'Split pane down',
118 action: () => console.log('Split down triggered')
119 },
120 {
121 key: 'X (pane mode)',
122 description: 'Close current pane',
123 action: () => console.log('Close pane triggered')
124 },
125 {
126 key: 'Enter (pane mode)',
127 description: 'Confirm action',
128 action: () => console.log('Confirm triggered')
129 },
130 {
131 key: 'Escape (pane mode)',
132 description: 'Exit pane mode',
133 action: () => console.log('Exit pane mode triggered')
134 },
135 // Workspace navigation
136 {
137 key: 'Alt+Arrow Keys',
138 description: 'Navigate between panes',
139 action: () => console.log('Navigate panes triggered')
140 }
141 ];
142 shortcutsLoading = false;
143 };
144
145 // Setup message bridge callbacks (once)
146 let callbacksInitialized = false;
147 const pendingRequests = new Map();
148
149 const initializeCallbacks = () => {
150 if (callbacksInitialized) return;
151
152 (window as any).__dumber_history_recent = (data: any[], requestId?: string) => {
153 const request = pendingRequests.get(requestId || 'default');
154 if (request) {
155 request.resolve(data);
156 pendingRequests.delete(requestId || 'default');
157 }
158 };
159
160 (window as any).__dumber_history_error = (error: string, requestId?: string) => {
161 const request = pendingRequests.get(requestId || 'default');
162 if (request) {
163 request.reject(new Error(error));
164 pendingRequests.delete(requestId || 'default');
165 }
166 };
167
168 // Omnibox suggestions callback - forward to injected script via DOM event bridge
169 let _pendingSuggestions: any[] | null = null;
170 (window as any).__dumber_omnibox_suggestions = (suggestions: any[]) => {
171 try {
172 console.log('🎯 [HOMEPAGE] Received omnibox suggestions:', suggestions?.length || 0, 'items');
173 // Dispatch an event that injected-world listener can consume
174 const evt = new CustomEvent('dumber:omnibox-suggestions', {
175 detail: { suggestions }
176 });
177 document.dispatchEvent(evt);
178 _pendingSuggestions = null; // clear cache on successful dispatch
179 } catch (err) {
180 console.warn('⚠️ [HOMEPAGE] Failed to dispatch suggestions event, caching:', err);
181 _pendingSuggestions = suggestions;
182 }
183 };
184
185 // If omnibox gets ready later, re-send any pending suggestions
186 document.addEventListener('dumber:omnibox-ready', () => {
187 if (_pendingSuggestions && Array.isArray(_pendingSuggestions)) {
188 try {
189 const evt = new CustomEvent('dumber:omnibox-suggestions', {
190 detail: { suggestions: _pendingSuggestions }
191 });
192 document.dispatchEvent(evt);
193 _pendingSuggestions = null;
194 console.log('🔄 [HOMEPAGE] Re-dispatched pending suggestions after omnibox-ready');
195 } catch (e) {
196 console.warn('⚠️ [HOMEPAGE] Failed to re-dispatch pending suggestions:', e);
197 }
198 }
199 });
200
201 callbacksInitialized = true;
202 };
203
204 const sendHistoryRequest = (type: string, params: any): Promise<any[]> => {
205 initializeCallbacks();
206
207 const requestId = `${type}_${Date.now()}_${Math.random()}`;
208
209 return new Promise((resolve, reject) => {
210 pendingRequests.set(requestId, { resolve, reject });
211
212 // Timeout after 10 seconds
213 setTimeout(() => {
214 if (pendingRequests.has(requestId)) {
215 pendingRequests.delete(requestId);
216 reject(new Error('Request timeout'));
217 }
218 }, 10000);
219
220 try {
221 const bridge = (window as any).webkit?.messageHandlers?.dumber;
222 if (bridge && typeof bridge.postMessage === 'function') {
223 bridge.postMessage(JSON.stringify({
224 ...params,
225 type,
226 requestId
227 }));
228 } else {
229 reject(new Error('WebKit message handler not available'));
230 }
231 } catch (error) {
232 pendingRequests.delete(requestId);
233 reject(error);
234 }
235 });
236 };
237
238 // Fetch recent history via message bridge
239 const fetchHistory = async () => {
240 try {
241 const data = await sendHistoryRequest('history_recent', {
242 limit: 20,
243 offset: 0
244 });
245
246 historyItems = Array.isArray(data) ? data : [];
247 historyOffset = historyItems.length;
248 hasMoreHistory = data.length === 20;
249 } catch (error) {
250 console.error('Error fetching history:', error);
251 historyItems = [];
252 hasMoreHistory = false;
253 } finally {
254 historyLoading = false;
255 }
256 };
257
258 // Load more history items for infinite scroll
259 const loadMoreHistory = async () => {
260 if (loadingMoreHistory || !hasMoreHistory) return;
261
262 loadingMoreHistory = true;
263 try {
264 const data = await sendHistoryRequest('history_recent', {
265 limit: 20,
266 offset: historyOffset
267 });
268
269 if (Array.isArray(data) && data.length > 0) {
270 historyItems = [...historyItems, ...data];
271 historyOffset += data.length;
272 hasMoreHistory = data.length === 20; // If we got full batch, there might be more
273 } else {
274 hasMoreHistory = false;
275 }
276 } catch (error) {
277 console.error('Error fetching more history:', error);
278 hasMoreHistory = false;
279 } finally {
280 loadingMoreHistory = false;
281 }
282 };
283
284 // Navigate to a URL when history item is clicked
285 const navigateTo = (url: string) => {
286 window.location.href = url;
287 };
288
289 // Delete a history entry
290 const deleteHistoryEntry = async (id: number, event: Event) => {
291 event.stopPropagation(); // Prevent navigation
292
293 // Add to deleting array for animation
294 if (!deletingIds.includes(id)) {
295 deletingIds = [...deletingIds, id];
296 }
297
298 try {
299 // Set up response handlers
300 (window as any).__dumber_history_deleted = async () => {
301 // Wait a bit for the fade animation to complete
302 await new Promise(resolve => setTimeout(resolve, 200));
303
304 // Reset pagination and refresh history after successful deletion
305 historyOffset = 0;
306 hasMoreHistory = true;
307 await fetchHistory();
308 await fetchStats();
309 await fetchTopVisited();
310
311 // Remove from deleting array after refresh
312 deletingIds = deletingIds.filter(i => i !== id);
313 };
314
315 (window as any).__dumber_history_error = (error: string) => {
316 console.error('Error deleting history entry:', error);
317 // Remove from deleting array if failed
318 deletingIds = deletingIds.filter(i => i !== id);
319 };
320
321 // Send message to Go backend
322 const bridge = (window as any).webkit?.messageHandlers?.dumber;
323 if (bridge && typeof bridge.postMessage === 'function') {
324 bridge.postMessage(JSON.stringify({
325 type: 'history_delete',
326 historyId: id.toString()
327 }));
328 } else {
329 throw new Error('WebKit message handler not available');
330 }
331 } catch (error) {
332 console.error('Error sending delete request:', error);
333 // Remove from deleting array if failed
334 deletingIds = deletingIds.filter(i => i !== id);
335 }
336 };
337
338 // Get domain from URL
339 const getDomain = (url: string): string => {
340 try {
341 return new URL(url).hostname;
342 } catch {
343 return url;
344 }
345 };
346
347 // Format relative time
348 const formatTime = (timestamp: string): string => {
349 try {
350 const date = new Date(timestamp);
351 const now = new Date();
352 const diff = now.getTime() - date.getTime();
353
354 const minutes = Math.floor(diff / (1000 * 60));
355 const hours = Math.floor(diff / (1000 * 60 * 60));
356 const days = Math.floor(diff / (1000 * 60 * 60 * 24));
357
358 if (minutes < 1) return 'Just now';
359 if (minutes < 60) return `${minutes}m ago`;
360 if (hours < 24) return `${hours}h ago`;
361 if (days < 7) return `${days}d ago`;
362
363 return date.toLocaleDateString();
364 } catch {
365 return '';
366 }
367 };
368
369 // Fetch history stats via message bridge
370 const fetchStats = async () => {
371 try {
372 // Set up response handler
373 (window as any).__dumber_history_stats = (data: any) => {
374 stats = data;
375 statsLoading = false;
376 };
377
378 (window as any).__dumber_history_error = (error: string) => {
379 console.error('Error fetching stats:', error);
380 stats = null;
381 statsLoading = false;
382 };
383
384 // Send message to Go backend
385 const bridge = (window as any).webkit?.messageHandlers?.dumber;
386 if (bridge && typeof bridge.postMessage === 'function') {
387 bridge.postMessage(JSON.stringify({
388 type: 'history_stats'
389 }));
390 } else {
391 throw new Error('WebKit message handler not available');
392 }
393 } catch (error) {
394 console.error('Error sending stats request:', error);
395 stats = null;
396 statsLoading = false;
397 }
398 };
399
400 // Fetch top visited sites via message bridge
401 const fetchTopVisited = async () => {
402 try {
403 // Set up response handler
404 (window as any).__dumber_history_search = (data: any[]) => {
405 // Sort by visit_count in descending order and take top 5
406 const sortedData = Array.isArray(data) ?
407 data.sort((a, b) => (b.visit_count || 0) - (a.visit_count || 0)).slice(0, 5) : [];
408 topVisited = sortedData;
409 topVisitedLoading = false;
410 };
411
412 (window as any).__dumber_history_error = (error: string) => {
413 console.error('Error fetching top visited:', error);
414 topVisited = [];
415 topVisitedLoading = false;
416 };
417
418 // Send message to Go backend
419 const bridge = (window as any).webkit?.messageHandlers?.dumber;
420 if (bridge && typeof bridge.postMessage === 'function') {
421 bridge.postMessage(JSON.stringify({
422 type: 'history_search',
423 q: '',
424 limit: 50 // Get more results to sort properly
425 }));
426 } else {
427 throw new Error('WebKit message handler not available');
428 }
429 } catch (error) {
430 console.error('Error sending top visited request:', error);
431 topVisited = [];
432 topVisitedLoading = false;
433 }
434 };
435
436 // Intersection Observer for infinite scroll
437 let historyScrollSentinel = $state<HTMLElement | null>(null);
438
439 const setupInfiniteScroll = () => {
440 if (!historyScrollSentinel) return;
441
442 const observer = new IntersectionObserver(
443 (entries) => {
444 const entry = entries[0];
445 if (entry && entry.isIntersecting && hasMoreHistory && !loadingMoreHistory) {
446 loadMoreHistory();
447 }
448 },
449 {
450 threshold: 0.1,
451 rootMargin: '50px'
452 }
453 );
454
455 observer.observe(historyScrollSentinel);
456
457 return () => observer.disconnect();
458 };
459
460 onMount(() => {
461 initializeShortcuts();
462 fetchHistory().then(() => {
463 // Setup infinite scroll after initial history is loaded
464 setTimeout(setupInfiniteScroll, 100);
465 });
466 fetchStats();
467 fetchTopVisited();
468 });
469
470</script>
471
472<div class="homepage-container">
473<div class="container">
474 <div class="history-section">
475 <h2 class="section-title">Recent History</h2>
476 <div class="history-list">
477 {#if historyLoading}
478 <div class="loading">Loading history...</div>
479 {:else if historyItems.length === 0}
480 <div class="empty-state">
481 <h3>No history yet</h3>
482 <p>Start browsing to see your recent history here.</p>
483 </div>
484 {:else}
485 {#each historyItems as item (item.url)}
486 <div
487 class="history-item"
488 class:deleting={isDeleting(item.id)}
489 role="button"
490 tabindex="0"
491 onclick={() => navigateTo(item.url)}
492 onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
493 >
494 <div class="history-line">
495 <div class="history-favicon-chip">
496 {#if item.favicon_url}
497 <img
498 src={item.favicon_url}
499 alt=""
500 class="history-favicon-img"
501 onerror={(e) => {
502 const target = e.target as HTMLImageElement;
503 if (target) {
504 target.style.display='none';
505 const fallback = target.nextElementSibling as HTMLElement;
506 if (fallback) fallback.style.display='block';
507 }
508 }}
509 onload={(e) => {
510 const target = e.target as HTMLImageElement;
511 if (target) {
512 const fallback = target.nextElementSibling as HTMLElement;
513 if (fallback) fallback.style.display='none';
514 }
515 }}
516 />
517 <div class="history-favicon-fallback" style="display: none;">
518 <!-- inline globe placeholder -->
519 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
520 <circle cx="12" cy="12" r="10" />
521 <path d="M2 12h20" />
522 </svg>
523 </div>
524 {:else}
525 <div class="history-favicon-fallback">
526 <!-- inline globe placeholder -->
527 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
528 <circle cx="12" cy="12" r="10" />
529 <path d="M2 12h20" />
530 </svg>
531 </div>
532 {/if}
533 </div>
534 <div class="history-title">{item.title || 'Untitled'}</div>
535 <div class="history-sep">•</div>
536 <div class="history-domain">{getDomain(item.url)}</div>
537 <div class="history-sep">•</div>
538 <div class="history-url">{item.url}</div>
539 <div class="history-time">{formatTime(item.last_visited)}</div>
540 <div
541 class="history-delete"
542 role="button"
543 tabindex="0"
544 onclick={(e) => deleteHistoryEntry(item.id, e)}
545 onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)}
546 title="Delete this entry"
547 >
548 <!-- inline delete placeholder -->
549 <span aria-hidden="true" style="display:inline-block;line-height:1;">×</span>
550 </div>
551 </div>
552 </div>
553 {/each}
554
555 <!-- Infinite scroll sentinel and loading indicator -->
556 {#if hasMoreHistory}
557 <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div>
558 {#if loadingMoreHistory}
559 <div class="loading-more">Loading more history...</div>
560 {/if}
561 {/if}
562 {/if}
563 </div>
564 </div>
565
566 <div class="shortcuts-section">
567 <h2 class="section-title">Keyboard Shortcuts</h2>
568 <div class="shortcuts-container">
569 {#if shortcutsLoading}
570 <div class="loading">Loading shortcuts...</div>
571 {:else}
572 <div class="shortcuts-card">
573 {#each shortcuts as shortcut, index (shortcut.key)}
574 <div class="shortcut-item">
575 <div class="shortcut-key-badge">{shortcut.key}</div>
576 <div class="shortcut-desc">{shortcut.description}</div>
577 </div>
578 {#if index < shortcuts.length - 1}
579 <div class="shortcut-separator"></div>
580 {/if}
581 {/each}
582 </div>
583 {/if}
584 </div>
585
586 <h2 class="section-title top-visited-title">Top 5 Most Visited</h2>
587 <div class="top-visited-container">
588 {#if topVisitedLoading}
589 <div class="loading">Loading top visited...</div>
590 {:else if topVisited.length === 0}
591 <div class="empty-state">
592 <p>No visit data available</p>
593 </div>
594 {:else}
595 <div class="top-visited-list">
596 {#each topVisited as item, index (item.url)}
597 <div
598 class="top-visited-item"
599 role="button"
600 tabindex="0"
601 onclick={() => navigateTo(item.url)}
602 onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)}
603 >
604 <div class="rank">{index + 1}</div>
605 <div class="site-info">
606 <div class="site-title">{item.title || 'Untitled'}</div>
607 <div class="site-domain">{getDomain(item.url)}</div>
608 </div>
609 <div class="visit-count">{item.visit_count}</div>
610 </div>
611 {/each}
612 </div>
613 {/if}
614 </div>
615
616 <h2 class="section-title stats-title">Statistics</h2>
617 <div class="stats-container">
618 {#if statsLoading}
619 <div class="loading">Loading stats...</div>
620 {:else if stats}
621 <div class="stats-grid">
622 <div class="stat-item">
623 <div class="stat-value">{stats.total_entries}</div>
624 <div class="stat-label">History Entries</div>
625 </div>
626 </div>
627 {:else}
628 <div class="empty-state">
629 <p>No statistics available</p>
630 </div>
631 {/if}
632 </div>
633 </div>
634</div>
635
636<Footer />
637</div>
638
639<style>
640 /* Remove default browser margins/padding on the page */
641 :global(html), :global(body) {
642 margin: 0;
643 padding: 0;
644 background: #1a1a1a;
645 }
646
647 /* Scoped homepage container to prevent global CSS conflicts */
648 .homepage-container {
649 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
650 background: #1a1a1a;
651 color: #ffffff;
652 min-height: 100vh;
653 overflow-x: hidden;
654 display: flex;
655 flex-direction: column;
656 margin: 0;
657 padding: 0;
658 box-sizing: border-box;
659 }
660
661 /* Reset styles for children, but scoped to homepage only */
662 .homepage-container * {
663 margin: 0;
664 padding: 0;
665 box-sizing: border-box;
666 }
667
668 .container {
669 flex: 1;
670 display: flex;
671 align-items: stretch;
672 max-width: 1200px;
673 margin: 0 auto;
674 width: 100%;
675 padding: 2rem;
676 gap: 2rem;
677 overflow-x: hidden;
678 }
679
680 .history-section {
681 flex: 2;
682 min-height: 0;
683 min-width: 0;
684 display: flex;
685 flex-direction: column;
686 }
687
688 .shortcuts-section {
689 flex: 1;
690 min-height: 0;
691 min-width: 0;
692 }
693
694 .section-title {
695 font-size: 1.5rem;
696 margin-bottom: 1rem;
697 color: #ffffff;
698 border-bottom: 2px solid #404040;
699 padding-bottom: 0.5rem;
700 }
701
702 .history-list {
703 overflow-y: auto;
704 overflow-x: hidden;
705 max-width: 100%;
706 max-height: calc(100vh - 8rem);
707 }
708
709 .history-item {
710 padding: 0.75rem;
711 margin-bottom: 0.5rem;
712 background: #2d2d2d;
713 border-radius: 6px;
714 cursor: pointer;
715 transition: all 0.2s ease-in-out;
716 border-left: 3px solid #404040;
717 overflow: hidden;
718 max-width: 100%;
719 position: relative;
720 opacity: 1;
721 transform: translateX(0);
722 }
723
724 .history-item:hover {
725 background: #3d3d3d;
726 border-left-color: #0066cc;
727 }
728
729 .history-item:hover .history-delete {
730 opacity: 1;
731 visibility: visible;
732 }
733
734 .history-item.deleting {
735 opacity: 0;
736 transform: translateX(-20px);
737 pointer-events: none;
738 }
739
740 .history-line {
741 display: flex;
742 gap: 0.5rem;
743 white-space: nowrap;
744 align-items: center;
745 overflow: hidden;
746 width: 100%;
747 min-width: 0;
748 }
749
750 .history-favicon-chip {
751 flex: 0 0 20px;
752 width: 20px;
753 height: 20px;
754 border-radius: 50%;
755 background: #ccc;
756 border: 1px solid rgba(0,0,0,.12);
757 box-shadow: 0 1px 2px rgba(0,0,0,.12);
758 display: flex;
759 align-items: center;
760 justify-content: center;
761 }
762
763 .history-favicon-img {
764 width: 16px;
765 height: 16px;
766 filter: brightness(1.06) contrast(1.03);
767 image-rendering: -webkit-optimize-contrast;
768 }
769
770 .history-favicon-fallback {
771 font-size: 12px;
772 display: flex;
773 align-items: center;
774 justify-content: center;
775 color: #888;
776 }
777
778 .history-title {
779 font-size: 0.95rem;
780 color: #e6e6e6;
781 flex: 0 0 auto;
782 }
783
784 .history-domain {
785 font-size: 0.9rem;
786 color: #a5a5a5;
787 flex: 0 0 auto;
788 }
789
790 .history-sep {
791 color: #666;
792 flex: 0 0 auto;
793 }
794
795 .history-url {
796 font-size: 0.9rem;
797 color: #7a7a7a;
798 flex: 1 1 auto;
799 min-width: 0;
800 overflow: hidden;
801 text-overflow: ellipsis;
802 -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
803 mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%);
804 }
805
806 .history-time {
807 font-size: 0.85rem;
808 color: #666;
809 flex: 0 0 auto;
810 margin-left: auto;
811 }
812
813 .history-delete {
814 position: absolute;
815 right: 0.75rem;
816 top: 50%;
817 transform: translateY(-50%);
818 opacity: 0;
819 visibility: hidden;
820 transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
821 cursor: pointer;
822 background: #1a1a1a;
823 border-radius: 3px;
824 padding: 0.25rem;
825 border: 1px solid #404040;
826 display: flex;
827 align-items: center;
828 justify-content: center;
829 width: 2rem;
830 height: 2rem;
831 color: #888;
832 }
833
834 .history-delete:hover {
835 background: #ff4444;
836 border-color: #ff6666;
837 color: #fff;
838 transform: translateY(-50%) scale(1.1);
839 }
840
841 .shortcuts-container {
842 /* Remove height limit to show all shortcuts */
843 }
844
845 .shortcuts-card {
846 background: #2d2d2d;
847 border-radius: 8px;
848 border: 1px solid #404040;
849 padding: 1rem;
850 }
851
852 .shortcut-item {
853 display: flex;
854 align-items: center;
855 gap: 0.75rem;
856 padding: 0.5rem 0;
857 transition: all 0.2s ease;
858 }
859
860 .shortcut-item:hover {
861 background: rgba(255, 255, 255, 0.02);
862 border-radius: 4px;
863 padding: 0.5rem;
864 margin: 0 -0.5rem;
865 }
866
867 .shortcut-separator {
868 height: 1px;
869 background: #404040;
870 margin: 0.25rem 0;
871 }
872
873 .shortcut-key-badge {
874 background: linear-gradient(135deg, #0066cc, #004499);
875 color: #ffffff;
876 padding: 0.25rem 0.5rem;
877 border-radius: 4px;
878 font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
879 font-size: 0.75rem;
880 font-weight: 600;
881 white-space: nowrap;
882 box-shadow: 0 1px 2px rgba(0, 102, 204, 0.2);
883 border: 1px solid rgba(255, 255, 255, 0.1);
884 min-width: fit-content;
885 }
886
887 .shortcut-desc {
888 color: #e6e6e6;
889 font-size: 0.9rem;
890 flex: 1;
891 }
892
893 .stats-title {
894 margin-top: 2rem;
895 }
896
897 .stats-container {
898 margin-top: 1rem;
899 }
900
901 .stats-grid {
902 display: flex;
903 gap: 1rem;
904 }
905
906 .stat-item {
907 background: #2d2d2d;
908 border-radius: 6px;
909 padding: 1rem;
910 text-align: center;
911 flex: 1;
912 border: 1px solid #404040;
913 }
914
915 .stat-value {
916 font-size: 1.5rem;
917 font-weight: bold;
918 color: #0066cc;
919 margin-bottom: 0.25rem;
920 }
921
922 .stat-label {
923 font-size: 0.8rem;
924 color: #888;
925 text-transform: uppercase;
926 letter-spacing: 0.5px;
927 }
928
929 .top-visited-title {
930 margin-top: 2rem;
931 }
932
933 .top-visited-container {
934 margin-top: 1rem;
935 }
936
937 .top-visited-list {
938 display: flex;
939 flex-direction: column;
940 gap: 0.5rem;
941 }
942
943 .top-visited-item {
944 display: flex;
945 align-items: center;
946 padding: 0.75rem;
947 background: #2d2d2d;
948 border-radius: 6px;
949 cursor: pointer;
950 transition: background 0.2s;
951 border: 1px solid #404040;
952 gap: 1rem;
953 }
954
955 .top-visited-item:hover {
956 background: #3d3d3d;
957 border-color: #0066cc;
958 }
959
960 .rank {
961 font-weight: bold;
962 color: #0066cc;
963 font-size: 1.1rem;
964 min-width: 1.5rem;
965 text-align: center;
966 }
967
968 .site-info {
969 flex: 1;
970 min-width: 0;
971 }
972
973 .site-title {
974 font-size: 0.9rem;
975 color: #e6e6e6;
976 margin-bottom: 0.2rem;
977 white-space: nowrap;
978 overflow: hidden;
979 text-overflow: ellipsis;
980 }
981
982 .site-domain {
983 font-size: 0.8rem;
984 color: #888;
985 }
986
987 .visit-count {
988 font-size: 0.85rem;
989 color: #0066cc;
990 font-weight: bold;
991 min-width: 2rem;
992 text-align: center;
993 }
994
995 .loading {
996 padding: 2rem;
997 text-align: center;
998 color: #888;
999 }
1000
1001 .scroll-sentinel {
1002 height: 1px;
1003 width: 100%;
1004 }
1005
1006 .loading-more {
1007 padding: 1rem;
1008 text-align: center;
1009 color: #888;
1010 font-size: 0.9rem;
1011 }
1012
1013 .empty-state {
1014 padding: 3rem 2rem;
1015 text-align: center;
1016 color: #666;
1017 }
1018
1019 .empty-state h3 {
1020 margin-bottom: 1rem;
1021 color: #888;
1022 }
1023
1024 @media (max-width: 768px) {
1025 .container {
1026 flex-direction: column;
1027 padding: 1rem;
1028 }
1029 }
1030</style>