feat: add pin/favorite sites and improve homepage UX
- Add pin functionality for websites from history and quick access - Pin/unpin with star icons, localStorage persistence - Pinned sites prioritized in "Jump back in" section - Remove domain from history titles, highlight in URLs instead - Replace ellipsis with gradient fade for long titles and URLs - Remove 20-item limit, show all sites with 2+ visits - Fix mobile URL overflow with proper width constraints - Increase initial history display from 10 to 15 items
- commit
- d84a0c8783159c7b804ba01f74ae382e533c18f3
- author
- bamen · · 2025-09-25 04:22 UTC
- parent
- e9c8076
- signature
- unsigned
2 files · +446 -434
M CHANGELOG.md +9 -0
| @@ -4,7 +4,16 @@ All notable changes to this project will be documented in this file. | |||
| 4 | 4 | ||
| 5 | 5 | ## [Unreleased] | |
| 6 | 6 | ||
| 7 | + | ### Added | |
| 8 | + | - **Pin/favorite sites**: Pin websites from history or quick access to prioritize them in "Jump back in" section. Uses localStorage persistence with star icons for pin/unpin actions | |
| 9 | + | ||
| 10 | + | ### Changed | |
| 11 | + | - **History item layout**: Removed "• domain" from titles, now highlight domain within the full URL using same color as title text | |
| 12 | + | - **Quick access logic**: Removed 20-item limit, now shows all sites with 2+ visits (pinned sites bypass visit requirement) | |
| 13 | + | - **History pagination**: Show 15 items initially (up from 10) with "Show N more" button | |
| 14 | + | ||
| 7 | 15 | ### Fixed | |
| 16 | + | - **Mobile URL overflow**: Fixed long URLs breaking layout on narrow screens by adding proper width constraints and CSS fixes | |
| 8 | 17 | - **GTK shortcut GLib-CRITICAL error**: Fixed NULL string assertion in shortcut callback system | |
| 9 | 18 | ||
| 10 | 19 | ## [0.10.0] - 2025-09-24 | |
| @@ -4,7 +4,16 @@ All notable changes to this project will be documented in this file. | |||||
| 4 | 4 | ||||
| 5 | ## [Unreleased] | 5 | ## [Unreleased] | ||
| 6 | 6 | ||||
| 7 | + | ### Added | |||
| 8 | + | - **Pin/favorite sites**: Pin websites from history or quick access to prioritize them in "Jump back in" section. Uses localStorage persistence with star icons for pin/unpin actions | |||
| 9 | + | ||||
| 10 | + | ### Changed | |||
| 11 | + | - **History item layout**: Removed "• domain" from titles, now highlight domain within the full URL using same color as title text | |||
| 12 | + | - **Quick access logic**: Removed 20-item limit, now shows all sites with 2+ visits (pinned sites bypass visit requirement) | |||
| 13 | + | - **History pagination**: Show 15 items initially (up from 10) with "Show N more" button | |||
| 14 | + | ||||
| 7 | ### Fixed | 15 | ### Fixed | ||
| 16 | + | - **Mobile URL overflow**: Fixed long URLs breaking layout on narrow screens by adding proper width constraints and CSS fixes | |||
| 8 | - **GTK shortcut GLib-CRITICAL error**: Fixed NULL string assertion in shortcut callback system | 17 | - **GTK shortcut GLib-CRITICAL error**: Fixed NULL string assertion in shortcut callback system | ||
| 9 | 18 | ||||
| 10 | ## [0.10.0] - 2025-09-24 | 19 | ## [0.10.0] - 2025-09-24 | ||
M gui/src/pages/Homepage.svelte +437 -434
| @@ -38,54 +38,57 @@ | |||
| 38 | 38 | let historyOffset = $state(0); | |
| 39 | 39 | let hasMoreHistory = $state(true); | |
| 40 | 40 | let loadingMoreHistory = $state(false); | |
| 41 | - | const MAX_QUICK_ACCESS_ITEMS = 9; | |
| 41 | + | const MIN_VISIT_COUNT_FOR_QUICK_ACCESS = 2; | |
| 42 | 42 | type ThemeMode = "light" | "dark"; | |
| 43 | 43 | const THEME_STORAGE_KEY = "dumber.theme"; | |
| 44 | 44 | let themeMode = $state<ThemeMode>("dark"); | |
| 45 | 45 | let themeObserver: MutationObserver | null = null; | |
| 46 | - | let historyHeaderRef = $state<HTMLElement | null>(null); | |
| 47 | - | let historyBodyRef = $state<HTMLElement | null>(null); | |
| 48 | - | let insightsPanelRef = $state<HTMLElement | null>(null); | |
| 49 | - | let resizeSyncRaf = 0; | |
| 46 | + | const INITIAL_HISTORY_DISPLAY = 15; | |
| 47 | + | let showAllHistory = $state(false); | |
| 48 | + | const PINNED_SITES_STORAGE_KEY = "dumber.pinnedSites"; | |
| 49 | + | let pinnedSites = $state<Set<string>>(new Set()); | |
| 50 | + | let displayedHistoryItems = $derived( | |
| 51 | + | showAllHistory ? historyItems : historyItems.slice(0, INITIAL_HISTORY_DISPLAY) | |
| 52 | + | ); | |
| 50 | 53 | ||
| 51 | - | const syncHistoryPanelSize = () => { | |
| 52 | - | if (!historyBodyRef || !historyHeaderRef || !insightsPanelRef) return; | |
| 54 | + | // Function for checking if an item is being deleted | |
| 55 | + | const isDeleting = (id: number) => deletingIds.includes(id); | |
| 53 | 56 | ||
| 54 | - | if (window.innerWidth <= 960) { | |
| 55 | - | historyBodyRef.style.maxHeight = ""; | |
| 56 | - | return; | |
| 57 | + | // Pinned sites management | |
| 58 | + | const loadPinnedSites = () => { | |
| 59 | + | try { | |
| 60 | + | const stored = localStorage.getItem(PINNED_SITES_STORAGE_KEY); | |
| 61 | + | if (stored) { | |
| 62 | + | const urls = JSON.parse(stored) as string[]; | |
| 63 | + | pinnedSites = new Set(urls); | |
| 64 | + | } | |
| 65 | + | } catch (error) { | |
| 66 | + | console.warn("[homepage] Failed to load pinned sites", error); | |
| 67 | + | pinnedSites = new Set(); | |
| 57 | 68 | } | |
| 69 | + | }; | |
| 58 | 70 | ||
| 59 | - | const insightsHeight = insightsPanelRef.getBoundingClientRect().height; | |
| 60 | - | if (!insightsHeight || Number.isNaN(insightsHeight)) { | |
| 61 | - | historyBodyRef.style.maxHeight = ""; | |
| 62 | - | return; | |
| 71 | + | const savePinnedSites = () => { | |
| 72 | + | try { | |
| 73 | + | const urls = Array.from(pinnedSites); | |
| 74 | + | localStorage.setItem(PINNED_SITES_STORAGE_KEY, JSON.stringify(urls)); | |
| 75 | + | } catch (error) { | |
| 76 | + | console.warn("[homepage] Failed to save pinned sites", error); | |
| 63 | 77 | } | |
| 78 | + | }; | |
| 64 | 79 | ||
| 65 | - | const headerHeight = historyHeaderRef.getBoundingClientRect().height; | |
| 66 | - | const bodyStyles = getComputedStyle(historyBodyRef); | |
| 67 | - | const paddingTop = parseFloat(bodyStyles.paddingTop) || 0; | |
| 68 | - | const paddingBottom = parseFloat(bodyStyles.paddingBottom) || 0; | |
| 69 | - | const maxHeight = insightsHeight - headerHeight - paddingTop - paddingBottom; | |
| 80 | + | const isPinned = (url: string): boolean => pinnedSites.has(url); | |
| 70 | 81 | ||
| 71 | - | if (maxHeight > 0) { | |
| 72 | - | historyBodyRef.style.maxHeight = `${maxHeight}px`; | |
| 82 | + | const togglePin = (url: string) => { | |
| 83 | + | if (pinnedSites.has(url)) { | |
| 84 | + | pinnedSites.delete(url); | |
| 73 | 85 | } else { | |
| 74 | - | historyBodyRef.style.maxHeight = ""; | |
| 86 | + | pinnedSites.add(url); | |
| 75 | 87 | } | |
| 88 | + | pinnedSites = new Set(pinnedSites); // Trigger reactivity | |
| 89 | + | savePinnedSites(); | |
| 76 | 90 | }; | |
| 77 | 91 | ||
| 78 | - | const scheduleHistoryPanelSync = () => { | |
| 79 | - | if (resizeSyncRaf) cancelAnimationFrame(resizeSyncRaf); | |
| 80 | - | resizeSyncRaf = requestAnimationFrame(() => { | |
| 81 | - | resizeSyncRaf = 0; | |
| 82 | - | syncHistoryPanelSize(); | |
| 83 | - | }); | |
| 84 | - | }; | |
| 85 | - | ||
| 86 | - | // Function for checking if an item is being deleted | |
| 87 | - | const isDeleting = (id: number) => deletingIds.includes(id); | |
| 88 | - | ||
| 89 | 92 | // Initialize shortcuts (these are browser shortcuts, not dynamic from API) | |
| 90 | 93 | const initializeShortcuts = () => { | |
| 91 | 94 | shortcuts = [ | |
| @@ -396,6 +399,23 @@ | |||
| 396 | 399 | return domain || item.url; | |
| 397 | 400 | }; | |
| 398 | 401 | ||
| 402 | + | const highlightDomainInUrl = (url: string): string => { | |
| 403 | + | try { | |
| 404 | + | const urlObj = new URL(url); | |
| 405 | + | const domain = urlObj.hostname; | |
| 406 | + | const domainStart = url.indexOf(domain); | |
| 407 | + | ||
| 408 | + | if (domainStart === -1) return url; | |
| 409 | + | ||
| 410 | + | const beforeDomain = url.slice(0, domainStart); | |
| 411 | + | const afterDomain = url.slice(domainStart + domain.length); | |
| 412 | + | ||
| 413 | + | return `${beforeDomain}<span class="history-url-domain">${domain}</span>${afterDomain}`; | |
| 414 | + | } catch { | |
| 415 | + | return url; | |
| 416 | + | } | |
| 417 | + | }; | |
| 418 | + | ||
| 399 | 419 | // Format relative time | |
| 400 | 420 | const formatTime = (timestamp: string): string => { | |
| 401 | 421 | try { | |
| @@ -444,27 +464,42 @@ | |||
| 444 | 464 | ||
| 445 | 465 | const pushItem = (item: HistoryItem | null | undefined) => { | |
| 446 | 466 | if (!item) return; | |
| 447 | - | const domain = getDomain(item.url); | |
| 448 | - | const key = domain || item.url; | |
| 449 | - | if (seen.has(key)) return; | |
| 450 | - | seen.add(key); | |
| 467 | + | if (seen.has(item.url)) return; | |
| 468 | + | seen.add(item.url); | |
| 451 | 469 | picks.push(item); | |
| 452 | 470 | }; | |
| 453 | 471 | ||
| 472 | + | // First, add pinned sites (no visit count requirement for pinned) | |
| 454 | 473 | for (const item of topVisited ?? []) { | |
| 455 | - | pushItem(item); | |
| 456 | - | if (picks.length >= MAX_QUICK_ACCESS_ITEMS) return picks; | |
| 474 | + | if (pinnedSites.has(item.url)) { | |
| 475 | + | pushItem(item); | |
| 476 | + | } | |
| 457 | 477 | } | |
| 458 | 478 | ||
| 459 | 479 | for (const item of historyItems ?? []) { | |
| 460 | - | pushItem(item); | |
| 461 | - | if (picks.length >= MAX_QUICK_ACCESS_ITEMS) break; | |
| 480 | + | if (pinnedSites.has(item.url)) { | |
| 481 | + | pushItem(item); | |
| 482 | + | } | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | // Then add most visited sites (non-pinned, with minimum visit count) | |
| 486 | + | for (const item of topVisited ?? []) { | |
| 487 | + | if (!pinnedSites.has(item.url) && item.visit_count >= MIN_VISIT_COUNT_FOR_QUICK_ACCESS) { | |
| 488 | + | pushItem(item); | |
| 489 | + | } | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | // Finally add recent history (non-pinned, with minimum visit count) | |
| 493 | + | for (const item of historyItems ?? []) { | |
| 494 | + | if (!pinnedSites.has(item.url) && item.visit_count >= MIN_VISIT_COUNT_FOR_QUICK_ACCESS) { | |
| 495 | + | pushItem(item); | |
| 496 | + | } | |
| 462 | 497 | } | |
| 463 | 498 | ||
| 464 | 499 | return picks; | |
| 465 | 500 | }; | |
| 466 | 501 | ||
| 467 | - | let quickAccess = $derived<HistoryItem[]>(buildQuickAccess()); | |
| 502 | + | let quickAccess = $derived<HistoryItem[]>((pinnedSites.size, buildQuickAccess())); | |
| 468 | 503 | let quickAccessLoading = $derived(topVisitedLoading || historyLoading); | |
| 469 | 504 | ||
| 470 | 505 | const persistTheme = (mode: ThemeMode) => { | |
| @@ -598,6 +633,7 @@ | |||
| 598 | 633 | }; | |
| 599 | 634 | ||
| 600 | 635 | onMount(() => { | |
| 636 | + | loadPinnedSites(); | |
| 601 | 637 | initializeShortcuts(); | |
| 602 | 638 | fetchHistory().then(() => { | |
| 603 | 639 | // Setup infinite scroll after initial history is loaded | |
| @@ -619,33 +655,12 @@ | |||
| 619 | 655 | attributeFilter: ["class"], | |
| 620 | 656 | }); | |
| 621 | 657 | ||
| 622 | - | window.addEventListener('resize', scheduleHistoryPanelSync); | |
| 623 | - | ||
| 624 | 658 | return () => { | |
| 625 | 659 | themeObserver?.disconnect(); | |
| 626 | 660 | themeObserver = null; | |
| 627 | - | window.removeEventListener('resize', scheduleHistoryPanelSync); | |
| 628 | - | if (resizeSyncRaf) { | |
| 629 | - | cancelAnimationFrame(resizeSyncRaf); | |
| 630 | - | resizeSyncRaf = 0; | |
| 631 | - | } | |
| 632 | 661 | }; | |
| 633 | 662 | }); | |
| 634 | 663 | ||
| 635 | - | $effect(() => { | |
| 636 | - | historyItems; | |
| 637 | - | topVisited; | |
| 638 | - | stats; | |
| 639 | - | shortcuts; | |
| 640 | - | scheduleHistoryPanelSync(); | |
| 641 | - | }); | |
| 642 | - | ||
| 643 | - | $effect(() => { | |
| 644 | - | historyHeaderRef; | |
| 645 | - | historyBodyRef; | |
| 646 | - | insightsPanelRef; | |
| 647 | - | scheduleHistoryPanelSync(); | |
| 648 | - | }); | |
| 649 | 664 | ||
| 650 | 665 | </script> | |
| 651 | 666 | ||
| @@ -691,86 +706,13 @@ | |||
| 691 | 706 | {/if} | |
| 692 | 707 | </button> | |
| 693 | 708 | </div> | |
| 694 | - | ||
| 695 | - | <section class="hero-panel brutal-panel"> | |
| 696 | - | <div class="hero-heading"> | |
| 697 | - | <h1 class="hero-title">Jump back in</h1> | |
| 698 | - | <p class="hero-subtitle">Direct access to the sites you visit the most, plus what you just explored.</p> | |
| 699 | - | </div> | |
| 700 | - | <div class="quick-access-wrapper"> | |
| 701 | - | {#if quickAccessLoading} | |
| 702 | - | <div class="loading quick-access-loading">Preparing your shortcuts...</div> | |
| 703 | - | {:else if quickAccess.length === 0} | |
| 704 | - | <div class="empty-state quick-access-empty"> | |
| 705 | - | <h3>No quick links yet</h3> | |
| 706 | - | <p>Browse a few sites and we’ll surface your frequent destinations here.</p> | |
| 707 | - | </div> | |
| 708 | - | {:else} | |
| 709 | - | <div class="quick-access-grid"> | |
| 710 | - | {#each quickAccess as item (item.id)} | |
| 711 | - | <div | |
| 712 | - | class="quick-access-item" | |
| 713 | - | role="button" | |
| 714 | - | tabindex="0" | |
| 715 | - | onclick={() => navigateTo(item.url)} | |
| 716 | - | onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)} | |
| 717 | - | > | |
| 718 | - | <div class="quick-access-favicon"> | |
| 719 | - | {#if item.favicon_url} | |
| 720 | - | <img | |
| 721 | - | src={item.favicon_url} | |
| 722 | - | alt="" | |
| 723 | - | class="quick-access-favicon-img" | |
| 724 | - | onerror={(e) => { | |
| 725 | - | const target = e.target as HTMLImageElement; | |
| 726 | - | if (target) { | |
| 727 | - | target.style.display = 'none'; | |
| 728 | - | const fallback = target.nextElementSibling as HTMLElement; | |
| 729 | - | if (fallback) fallback.style.display = 'flex'; | |
| 730 | - | } | |
| 731 | - | }} | |
| 732 | - | onload={(e) => { | |
| 733 | - | const target = e.target as HTMLImageElement; | |
| 734 | - | if (target) { | |
| 735 | - | const fallback = target.nextElementSibling as HTMLElement; | |
| 736 | - | if (fallback) fallback.style.display = 'none'; | |
| 737 | - | } | |
| 738 | - | }} | |
| 739 | - | /> | |
| 740 | - | <div class="quick-access-fallback" style="display: none;"> | |
| 741 | - | <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |
| 742 | - | <circle cx="12" cy="12" r="10" /> | |
| 743 | - | <path d="M2 12h20" /> | |
| 744 | - | </svg> | |
| 745 | - | </div> | |
| 746 | - | {:else} | |
| 747 | - | <div class="quick-access-fallback"> | |
| 748 | - | <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |
| 749 | - | <circle cx="12" cy="12" r="10" /> | |
| 750 | - | <path d="M2 12h20" /> | |
| 751 | - | </svg> | |
| 752 | - | </div> | |
| 753 | - | {/if} | |
| 754 | - | </div> | |
| 755 | - | <div class="quick-access-meta"> | |
| 756 | - | <div class="quick-access-title">{getDisplayTitle(item)}</div> | |
| 757 | - | <div class="quick-access-domain">{getDomain(item.url)}</div> | |
| 758 | - | </div> | |
| 759 | - | <div class="quick-access-visits">{formatVisitLabel(item.visit_count)}</div> | |
| 760 | - | </div> | |
| 761 | - | {/each} | |
| 762 | - | </div> | |
| 763 | - | {/if} | |
| 764 | - | </div> | |
| 765 | - | </section> | |
| 766 | - | ||
| 767 | 709 | <div class="main-panels"> | |
| 768 | 710 | <section class="history-panel brutal-panel"> | |
| 769 | - | <div class="panel-header" bind:this={historyHeaderRef}> | |
| 711 | + | <div class="panel-header"> | |
| 770 | 712 | <h2 class="panel-title">Recent History</h2> | |
| 771 | 713 | <p class="panel-subtitle">Scroll to revisit anything from your latest sessions.</p> | |
| 772 | 714 | </div> | |
| 773 | - | <div class="panel-body history-body" bind:this={historyBodyRef}> | |
| 715 | + | <div class="panel-body history-body"> | |
| 774 | 716 | {#if historyLoading} | |
| 775 | 717 | <div class="loading">Loading history...</div> | |
| 776 | 718 | {:else if historyItems.length === 0} | |
| @@ -780,7 +722,7 @@ | |||
| 780 | 722 | </div> | |
| 781 | 723 | {:else} | |
| 782 | 724 | <div class="history-list"> | |
| 783 | - | {#each historyItems as item (item.id)} | |
| 725 | + | {#each displayedHistoryItems as item (item.id)} | |
| 784 | 726 | <div | |
| 785 | 727 | class="history-item" | |
| 786 | 728 | class:deleting={isDeleting(item.id)} | |
| @@ -827,29 +769,54 @@ | |||
| 827 | 769 | {/if} | |
| 828 | 770 | </div> | |
| 829 | 771 | <div class="history-item-content"> | |
| 830 | - | <div class="history-item-top"> | |
| 772 | + | <div class="history-item-main"> | |
| 831 | 773 | <div class="history-title">{getDisplayTitle(item)}</div> | |
| 832 | 774 | <div class="history-time">{formatTime(item.last_visited)}</div> | |
| 833 | 775 | </div> | |
| 834 | - | <div class="history-item-bottom"> | |
| 835 | - | <div class="history-domain">{getDomain(item.url)}</div> | |
| 836 | - | <div class="history-url">{item.url}</div> | |
| 837 | - | </div> | |
| 776 | + | <div class="history-url">{@html highlightDomainInUrl(item.url)}</div> | |
| 838 | 777 | </div> | |
| 839 | - | <div | |
| 840 | - | class="history-delete" | |
| 841 | - | role="button" | |
| 842 | - | tabindex="0" | |
| 843 | - | onclick={(e) => deleteHistoryEntry(item.id, e)} | |
| 844 | - | onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)} | |
| 845 | - | title="Delete this entry" | |
| 846 | - | > | |
| 847 | - | <span aria-hidden="true">×</span> | |
| 778 | + | <div class="history-actions"> | |
| 779 | + | <div | |
| 780 | + | class="history-pin" | |
| 781 | + | role="button" | |
| 782 | + | tabindex="0" | |
| 783 | + | onclick={(e) => { e.stopPropagation(); togglePin(item.url); }} | |
| 784 | + | onkeydown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); togglePin(item.url); } }} | |
| 785 | + | title={isPinned(item.url) ? "Unpin from favorites" : "Pin to favorites"} | |
| 786 | + | > | |
| 787 | + | {#if isPinned(item.url)} | |
| 788 | + | <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> | |
| 789 | + | <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> | |
| 790 | + | </svg> | |
| 791 | + | {:else} | |
| 792 | + | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"> | |
| 793 | + | <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> | |
| 794 | + | </svg> | |
| 795 | + | {/if} | |
| 796 | + | </div> | |
| 797 | + | <div | |
| 798 | + | class="history-delete" | |
| 799 | + | role="button" | |
| 800 | + | tabindex="0" | |
| 801 | + | onclick={(e) => deleteHistoryEntry(item.id, e)} | |
| 802 | + | onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)} | |
| 803 | + | title="Delete this entry" | |
| 804 | + | > | |
| 805 | + | <span aria-hidden="true">×</span> | |
| 806 | + | </div> | |
| 848 | 807 | </div> | |
| 849 | 808 | </div> | |
| 850 | 809 | {/each} | |
| 851 | 810 | ||
| 852 | - | {#if hasMoreHistory} | |
| 811 | + | {#if !showAllHistory && historyItems.length > INITIAL_HISTORY_DISPLAY} | |
| 812 | + | <button | |
| 813 | + | class="show-more-button" | |
| 814 | + | onclick={() => showAllHistory = true} | |
| 815 | + | type="button" | |
| 816 | + | > | |
| 817 | + | Show {historyItems.length - INITIAL_HISTORY_DISPLAY} more items | |
| 818 | + | </button> | |
| 819 | + | {:else if showAllHistory && hasMoreHistory} | |
| 853 | 820 | <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div> | |
| 854 | 821 | {#if loadingMoreHistory} | |
| 855 | 822 | <div class="loading-more">Loading more history...</div> | |
| @@ -860,75 +827,95 @@ | |||
| 860 | 827 | </div> | |
| 861 | 828 | </section> | |
| 862 | 829 | ||
| 863 | - | <section class="insights-panel brutal-panel" bind:this={insightsPanelRef}> | |
| 830 | + | <section class="quick-access-panel brutal-panel"> | |
| 864 | 831 | <div class="panel-header"> | |
| 865 | - | <h2 class="panel-title">Usage Insights</h2> | |
| 866 | - | <p class="panel-subtitle">Your browsing patterns at a glance.</p> | |
| 832 | + | <h2 class="panel-title">Jump back in</h2> | |
| 833 | + | <p class="panel-subtitle">Direct access to the sites you like the most.</p> | |
| 867 | 834 | </div> | |
| 868 | - | <div class="panel-body insights-body"> | |
| 869 | - | <div class="insight-block"> | |
| 870 | - | <h3 class="block-title">Top sites</h3> | |
| 871 | - | {#if topVisitedLoading} | |
| 872 | - | <div class="loading">Loading usage insights...</div> | |
| 873 | - | {:else if topVisited.length === 0} | |
| 874 | - | <div class="empty-state compact"> | |
| 875 | - | <p>No visit data available</p> | |
| 876 | - | </div> | |
| 877 | - | {:else} | |
| 878 | - | <div class="insights-list"> | |
| 879 | - | {#each topVisited as item, index (item.id)} | |
| 835 | + | <div class="panel-body quick-access-body"> | |
| 836 | + | {#if quickAccessLoading} | |
| 837 | + | <div class="loading">Preparing your shortcuts...</div> | |
| 838 | + | {:else if quickAccess.length === 0} | |
| 839 | + | <div class="empty-state compact"> | |
| 840 | + | <h3>No quick links yet</h3> | |
| 841 | + | <p>Browse a few sites and we'll surface your frequent destinations here.</p> | |
| 842 | + | </div> | |
| 843 | + | {:else} | |
| 844 | + | <div class="quick-access-grid-compact"> | |
| 845 | + | {#each quickAccess as item (item.id)} | |
| 846 | + | <div | |
| 847 | + | class="quick-access-item-compact" | |
| 848 | + | role="button" | |
| 849 | + | tabindex="0" | |
| 850 | + | onclick={() => navigateTo(item.url)} | |
| 851 | + | onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)} | |
| 852 | + | > | |
| 853 | + | <div class="quick-access-favicon-compact"> | |
| 854 | + | {#if item.favicon_url} | |
| 855 | + | <img | |
| 856 | + | src={item.favicon_url} | |
| 857 | + | alt="" | |
| 858 | + | class="quick-access-favicon-img-compact" | |
| 859 | + | onerror={(e) => { | |
| 860 | + | const target = e.target as HTMLImageElement; | |
| 861 | + | if (target) { | |
| 862 | + | target.style.display = 'none'; | |
| 863 | + | const fallback = target.nextElementSibling as HTMLElement; | |
| 864 | + | if (fallback) fallback.style.display = 'flex'; | |
| 865 | + | } | |
| 866 | + | }} | |
| 867 | + | onload={(e) => { | |
| 868 | + | const target = e.target as HTMLImageElement; | |
| 869 | + | if (target) { | |
| 870 | + | const fallback = target.nextElementSibling as HTMLElement; | |
| 871 | + | if (fallback) fallback.style.display = 'none'; | |
| 872 | + | } | |
| 873 | + | }} | |
| 874 | + | /> | |
| 875 | + | <div class="quick-access-fallback-compact" style="display: none;"> | |
| 876 | + | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |
| 877 | + | <circle cx="12" cy="12" r="10" /> | |
| 878 | + | <path d="M2 12h20" /> | |
| 879 | + | </svg> | |
| 880 | + | </div> | |
| 881 | + | {:else} | |
| 882 | + | <div class="quick-access-fallback-compact"> | |
| 883 | + | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |
| 884 | + | <circle cx="12" cy="12" r="10" /> | |
| 885 | + | <path d="M2 12h20" /> | |
| 886 | + | </svg> | |
| 887 | + | </div> | |
| 888 | + | {/if} | |
| 889 | + | </div> | |
| 890 | + | <div class="quick-access-meta-compact"> | |
| 891 | + | <div class="quick-access-title-compact">{getDisplayTitle(item)}</div> | |
| 892 | + | <div class="quick-access-domain-compact">{getDomain(item.url)}</div> | |
| 893 | + | </div> | |
| 894 | + | <div class="quick-access-actions-compact"> | |
| 880 | 895 | <div | |
| 881 | - | class="insight-item" | |
| 896 | + | class="quick-access-pin-compact" | |
| 882 | 897 | role="button" | |
| 883 | 898 | tabindex="0" | |
| 884 | - | onclick={() => navigateTo(item.url)} | |
| 885 | - | onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)} | |
| 899 | + | onclick={(e) => { e.stopPropagation(); togglePin(item.url); }} | |
| 900 | + | onkeydown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); togglePin(item.url); } }} | |
| 901 | + | title={isPinned(item.url) ? "Unpin from favorites" : "Pin to favorites"} | |
| 886 | 902 | > | |
| 887 | - | <div class="insight-rank">{index + 1}</div> | |
| 888 | - | <div class="insight-body"> | |
| 889 | - | <div class="insight-title">{getDisplayTitle(item)}</div> | |
| 890 | - | <div class="insight-domain">{getDomain(item.url)}</div> | |
| 891 | - | </div> | |
| 892 | - | <div class="insight-visits">{formatVisitLabel(item.visit_count)}</div> | |
| 903 | + | {#if isPinned(item.url)} | |
| 904 | + | <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> | |
| 905 | + | <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> | |
| 906 | + | </svg> | |
| 907 | + | {:else} | |
| 908 | + | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"> | |
| 909 | + | <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> | |
| 910 | + | </svg> | |
| 911 | + | {/if} | |
| 893 | 912 | </div> | |
| 894 | - | {/each} | |
| 895 | - | </div> | |
| 896 | - | {/if} | |
| 897 | - | </div> | |
| 898 | - | ||
| 899 | - | <div class="insight-block"> | |
| 900 | - | <h3 class="block-title">History snapshot</h3> | |
| 901 | - | {#if statsLoading} | |
| 902 | - | <div class="loading">Loading stats...</div> | |
| 903 | - | {:else if stats} | |
| 904 | - | <div class="stats-grid"> | |
| 905 | - | <div class="stat-item"> | |
| 906 | - | <div class="stat-value">{stats.total_entries}</div> | |
| 907 | - | <div class="stat-label">Entries Stored</div> | |
| 913 | + | <div class="quick-access-visits-compact">{formatVisitLabel(item.visit_count)}</div> | |
| 908 | 914 | </div> | |
| 909 | - | <div class="stat-item"> | |
| 910 | - | <div class="stat-value">{stats.recent_count}</div> | |
| 911 | - | <div class="stat-label">Recent Window</div> | |
| 912 | - | </div> | |
| 913 | - | {#if stats.newest_entry} | |
| 914 | - | <div class="stat-item"> | |
| 915 | - | <div class="stat-value">{formatTime(stats.newest_entry)}</div> | |
| 916 | - | <div class="stat-label">Newest Visit</div> | |
| 917 | - | </div> | |
| 918 | - | {/if} | |
| 919 | - | {#if stats.oldest_entry} | |
| 920 | - | <div class="stat-item"> | |
| 921 | - | <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div> | |
| 922 | - | <div class="stat-label">Oldest Entry Loaded</div> | |
| 923 | - | </div> | |
| 924 | - | {/if} | |
| 925 | - | </div> | |
| 926 | - | {:else} | |
| 927 | - | <div class="empty-state compact"> | |
| 928 | - | <p>No statistics available</p> | |
| 929 | 915 | </div> | |
| 930 | - | {/if} | |
| 916 | + | {/each} | |
| 931 | 917 | </div> | |
| 918 | + | {/if} | |
| 932 | 919 | </div> | |
| 933 | 920 | </section> | |
| 934 | 921 | </div> | |
| @@ -953,6 +940,47 @@ | |||
| 953 | 940 | {/if} | |
| 954 | 941 | </div> | |
| 955 | 942 | </section> | |
| 943 | + | ||
| 944 | + | ||
| 945 | + | <section class="stats-panel brutal-panel"> | |
| 946 | + | <div class="panel-header"> | |
| 947 | + | <h2 class="panel-title">Usage Stats</h2> | |
| 948 | + | <p class="panel-subtitle">Your browsing activity at a glance.</p> | |
| 949 | + | </div> | |
| 950 | + | <div class="panel-body"> | |
| 951 | + | {#if statsLoading} | |
| 952 | + | <div class="loading">Loading stats...</div> | |
| 953 | + | {:else if stats} | |
| 954 | + | <div class="stats-grid-horizontal"> | |
| 955 | + | <div class="stat-item-horizontal"> | |
| 956 | + | <div class="stat-value">{stats.total_entries}</div> | |
| 957 | + | <div class="stat-label">Entries Stored</div> | |
| 958 | + | </div> | |
| 959 | + | <div class="stat-item-horizontal"> | |
| 960 | + | <div class="stat-value">{stats.recent_count}</div> | |
| 961 | + | <div class="stat-label">Recent Window</div> | |
| 962 | + | </div> | |
| 963 | + | {#if stats.newest_entry} | |
| 964 | + | <div class="stat-item-horizontal"> | |
| 965 | + | <div class="stat-value">{formatTime(stats.newest_entry)}</div> | |
| 966 | + | <div class="stat-label">Newest Visit</div> | |
| 967 | + | </div> | |
| 968 | + | {/if} | |
| 969 | + | {#if stats.oldest_entry} | |
| 970 | + | <div class="stat-item-horizontal"> | |
| 971 | + | <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div> | |
| 972 | + | <div class="stat-label">Oldest Entry</div> | |
| 973 | + | </div> | |
| 974 | + | {/if} | |
| 975 | + | </div> | |
| 976 | + | {:else} | |
| 977 | + | <div class="empty-state compact"> | |
| 978 | + | <p>No statistics available</p> | |
| 979 | + | </div> | |
| 980 | + | {/if} | |
| 981 | + | </div> | |
| 982 | + | </section> | |
| 983 | + | ||
| 956 | 984 | </div> | |
| 957 | 985 | ||
| 958 | 986 | <Footer /> | |
| @@ -1039,126 +1067,7 @@ | |||
| 1039 | 1067 | gap: 0; | |
| 1040 | 1068 | } | |
| 1041 | 1069 | ||
| 1042 | - | .hero-panel { | |
| 1043 | - | gap: clamp(1.25rem, 2vw, 1.75rem); | |
| 1044 | - | padding: clamp(1.75rem, 2.2vw, 2.5rem); | |
| 1045 | - | } | |
| 1046 | - | ||
| 1047 | - | .hero-heading { | |
| 1048 | - | display: flex; | |
| 1049 | - | flex-direction: column; | |
| 1050 | - | gap: 0.5rem; | |
| 1051 | - | max-width: 36rem; | |
| 1052 | - | } | |
| 1053 | - | ||
| 1054 | - | .hero-title { | |
| 1055 | - | font-size: clamp(2rem, 3vw, 2.75rem); | |
| 1056 | - | font-weight: 700; | |
| 1057 | - | letter-spacing: -0.01em; | |
| 1058 | - | margin: 0; | |
| 1059 | - | } | |
| 1060 | - | ||
| 1061 | - | .hero-subtitle { | |
| 1062 | - | font-size: 1rem; | |
| 1063 | - | line-height: 1.6; | |
| 1064 | - | color: var(--dynamic-muted); | |
| 1065 | - | margin: 0; | |
| 1066 | - | } | |
| 1067 | - | ||
| 1068 | - | .quick-access-wrapper { | |
| 1069 | - | display: flex; | |
| 1070 | - | flex-direction: column; | |
| 1071 | - | gap: 1rem; | |
| 1072 | - | } | |
| 1073 | - | ||
| 1074 | - | .quick-access-grid { | |
| 1075 | - | display: flex; | |
| 1076 | - | flex-wrap: wrap; | |
| 1077 | - | gap: 1rem; | |
| 1078 | - | } | |
| 1079 | - | ||
| 1080 | - | .quick-access-item { | |
| 1081 | - | flex: 1 1 min(16rem, 100%); | |
| 1082 | - | min-width: 12rem; | |
| 1083 | - | display: flex; | |
| 1084 | - | align-items: center; | |
| 1085 | - | gap: 1rem; | |
| 1086 | - | border: 2px solid var(--dynamic-border); | |
| 1087 | - | background-color: var(--dynamic-surface-variant); | |
| 1088 | - | padding: 0.9rem 1.1rem; | |
| 1089 | - | cursor: pointer; | |
| 1090 | - | border-radius: 0; | |
| 1091 | - | transition: | |
| 1092 | - | border-color 200ms ease, | |
| 1093 | - | background-color 200ms ease; | |
| 1094 | - | } | |
| 1095 | - | ||
| 1096 | - | .quick-access-item:hover, | |
| 1097 | - | .quick-access-item:focus-visible { | |
| 1098 | - | border-color: var(--dynamic-accent); | |
| 1099 | - | outline: none; | |
| 1100 | - | } | |
| 1101 | - | ||
| 1102 | - | .quick-access-favicon { | |
| 1103 | - | width: 48px; | |
| 1104 | - | height: 48px; | |
| 1105 | - | border: 2px solid var(--dynamic-border); | |
| 1106 | - | background-color: var(--dynamic-surface); | |
| 1107 | - | display: flex; | |
| 1108 | - | align-items: center; | |
| 1109 | - | justify-content: center; | |
| 1110 | - | flex-shrink: 0; | |
| 1111 | - | border-radius: 0; | |
| 1112 | - | } | |
| 1113 | - | ||
| 1114 | - | .quick-access-favicon-img { | |
| 1115 | - | width: 70%; | |
| 1116 | - | height: 70%; | |
| 1117 | - | object-fit: contain; | |
| 1118 | - | } | |
| 1119 | - | ||
| 1120 | - | .quick-access-fallback { | |
| 1121 | - | width: 100%; | |
| 1122 | - | height: 100%; | |
| 1123 | - | display: flex; | |
| 1124 | - | align-items: center; | |
| 1125 | - | justify-content: center; | |
| 1126 | - | color: var(--dynamic-muted); | |
| 1127 | - | } | |
| 1128 | - | ||
| 1129 | - | .quick-access-meta { | |
| 1130 | - | flex: 1; | |
| 1131 | - | min-width: 0; | |
| 1132 | - | display: flex; | |
| 1133 | - | flex-direction: column; | |
| 1134 | - | gap: 0.125rem; | |
| 1135 | - | } | |
| 1136 | - | ||
| 1137 | - | .quick-access-title { | |
| 1138 | - | font-size: 1rem; | |
| 1139 | - | font-weight: 600; | |
| 1140 | - | color: var(--dynamic-text); | |
| 1141 | - | white-space: nowrap; | |
| 1142 | - | overflow: hidden; | |
| 1143 | - | text-overflow: ellipsis; | |
| 1144 | - | } | |
| 1145 | 1070 | ||
| 1146 | - | .quick-access-domain { | |
| 1147 | - | font-size: 0.85rem; | |
| 1148 | - | color: var(--dynamic-muted); | |
| 1149 | - | white-space: nowrap; | |
| 1150 | - | overflow: hidden; | |
| 1151 | - | text-overflow: ellipsis; | |
| 1152 | - | } | |
| 1153 | - | ||
| 1154 | - | .quick-access-visits { | |
| 1155 | - | font-size: 0.82rem; | |
| 1156 | - | color: var(--dynamic-accent); | |
| 1157 | - | text-transform: uppercase; | |
| 1158 | - | letter-spacing: 0.05em; | |
| 1159 | - | font-weight: 600; | |
| 1160 | - | flex-shrink: 0; | |
| 1161 | - | } | |
| 1162 | 1071 | ||
| 1163 | 1072 | .loading { | |
| 1164 | 1073 | padding: 1.25rem; | |
| @@ -1211,7 +1120,7 @@ | |||
| 1211 | 1120 | min-height: 0; | |
| 1212 | 1121 | } | |
| 1213 | 1122 | ||
| 1214 | - | .insights-panel { | |
| 1123 | + | .quick-access-panel { | |
| 1215 | 1124 | flex: 1 1 35%; | |
| 1216 | 1125 | min-width: min(24rem, 100%); | |
| 1217 | 1126 | display: flex; | |
| @@ -1264,28 +1173,31 @@ | |||
| 1264 | 1173 | flex: 1; | |
| 1265 | 1174 | min-height: 0; | |
| 1266 | 1175 | overflow-y: auto; | |
| 1267 | - | padding-right: 0.5rem; | |
| 1176 | + | background-color: var(--dynamic-surface-variant); | |
| 1177 | + | border: 2px solid var(--dynamic-border); | |
| 1178 | + | padding: 0.5rem; | |
| 1268 | 1179 | } | |
| 1269 | 1180 | ||
| 1270 | 1181 | .history-item { | |
| 1271 | 1182 | display: flex; | |
| 1272 | 1183 | align-items: flex-start; | |
| 1273 | - | gap: 1rem; | |
| 1274 | - | border: 2px solid var(--dynamic-border); | |
| 1184 | + | gap: 0.75rem; | |
| 1185 | + | border-bottom: 1px solid var(--dynamic-border); | |
| 1275 | 1186 | background-color: var(--dynamic-surface-variant); | |
| 1276 | - | padding: 1rem 1.1rem; | |
| 1187 | + | padding: 0.75rem 0.5rem; | |
| 1277 | 1188 | cursor: pointer; | |
| 1278 | - | min-height: 80px; | |
| 1279 | - | border-radius: 0; | |
| 1280 | 1189 | transition: | |
| 1281 | - | border-color 200ms ease, | |
| 1282 | - | background-color 200ms ease; | |
| 1190 | + | background-color 200ms ease, | |
| 1191 | + | border-color 200ms ease; | |
| 1192 | + | } | |
| 1193 | + | ||
| 1194 | + | .history-item:last-child { | |
| 1195 | + | border-bottom: none; | |
| 1283 | 1196 | } | |
| 1284 | 1197 | ||
| 1285 | 1198 | .history-item:hover, | |
| 1286 | 1199 | .history-item:focus-visible { | |
| 1287 | - | border-color: var(--dynamic-accent); | |
| 1288 | - | background-color: var(--dynamic-bg); | |
| 1200 | + | background-color: var(--dynamic-surface); | |
| 1289 | 1201 | outline: none; | |
| 1290 | 1202 | } | |
| 1291 | 1203 | ||
| @@ -1327,11 +1239,12 @@ | |||
| 1327 | 1239 | min-width: 0; | |
| 1328 | 1240 | display: flex; | |
| 1329 | 1241 | flex-direction: column; | |
| 1330 | - | gap: 0.125rem; | |
| 1242 | + | gap: 0.25rem; | |
| 1331 | 1243 | justify-content: center; | |
| 1244 | + | overflow: hidden; | |
| 1332 | 1245 | } | |
| 1333 | 1246 | ||
| 1334 | - | .history-item-top { | |
| 1247 | + | .history-item-main { | |
| 1335 | 1248 | display: flex; | |
| 1336 | 1249 | align-items: flex-start; | |
| 1337 | 1250 | justify-content: space-between; | |
| @@ -1343,31 +1256,19 @@ | |||
| 1343 | 1256 | color: var(--dynamic-text); | |
| 1344 | 1257 | white-space: nowrap; | |
| 1345 | 1258 | overflow: hidden; | |
| 1346 | - | text-overflow: ellipsis; | |
| 1259 | + | flex: 1; | |
| 1260 | + | position: relative; | |
| 1261 | + | mask-image: linear-gradient(to right, black 70%, transparent 100%); | |
| 1262 | + | -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%); | |
| 1347 | 1263 | } | |
| 1348 | 1264 | ||
| 1265 | + | ||
| 1349 | 1266 | .history-time { | |
| 1350 | 1267 | font-size: 0.85rem; | |
| 1351 | 1268 | color: var(--dynamic-muted); | |
| 1352 | 1269 | flex-shrink: 0; | |
| 1353 | 1270 | } | |
| 1354 | 1271 | ||
| 1355 | - | .history-item-bottom { | |
| 1356 | - | display: flex; | |
| 1357 | - | align-items: baseline; | |
| 1358 | - | gap: 0.5rem; | |
| 1359 | - | overflow: hidden; | |
| 1360 | - | position: relative; | |
| 1361 | - | } | |
| 1362 | - | ||
| 1363 | - | .history-domain { | |
| 1364 | - | font-size: 0.9rem; | |
| 1365 | - | font-weight: 500; | |
| 1366 | - | color: var(--dynamic-text); | |
| 1367 | - | white-space: nowrap; | |
| 1368 | - | flex-shrink: 0; | |
| 1369 | - | } | |
| 1370 | - | ||
| 1371 | 1272 | .history-url { | |
| 1372 | 1273 | font-size: 0.8rem; | |
| 1373 | 1274 | color: var(--dynamic-muted); | |
| @@ -1379,11 +1280,41 @@ | |||
| 1379 | 1280 | monospace; | |
| 1380 | 1281 | overflow: hidden; | |
| 1381 | 1282 | flex: 1; | |
| 1283 | + | min-width: 0; | |
| 1382 | 1284 | position: relative; | |
| 1383 | 1285 | mask-image: linear-gradient(to right, black 70%, transparent 100%); | |
| 1384 | 1286 | -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%); | |
| 1385 | 1287 | } | |
| 1386 | 1288 | ||
| 1289 | + | .history-url :global(.history-url-domain) { | |
| 1290 | + | color: var(--dynamic-text) !important; | |
| 1291 | + | font-weight: 600; | |
| 1292 | + | } | |
| 1293 | + | ||
| 1294 | + | .history-actions { | |
| 1295 | + | display: flex; | |
| 1296 | + | flex-shrink: 0; | |
| 1297 | + | } | |
| 1298 | + | ||
| 1299 | + | .history-pin { | |
| 1300 | + | display: flex; | |
| 1301 | + | align-items: center; | |
| 1302 | + | justify-content: center; | |
| 1303 | + | width: 38px; | |
| 1304 | + | border-left: 2px solid var(--dynamic-border); | |
| 1305 | + | background-color: var(--dynamic-surface); | |
| 1306 | + | color: var(--dynamic-muted); | |
| 1307 | + | transition: background-color 200ms ease, color 200ms ease; | |
| 1308 | + | cursor: pointer; | |
| 1309 | + | } | |
| 1310 | + | ||
| 1311 | + | .history-pin:hover, | |
| 1312 | + | .history-pin:focus-visible { | |
| 1313 | + | color: var(--dynamic-accent); | |
| 1314 | + | background-color: var(--dynamic-bg); | |
| 1315 | + | outline: none; | |
| 1316 | + | } | |
| 1317 | + | ||
| 1387 | 1318 | .history-delete { | |
| 1388 | 1319 | display: flex; | |
| 1389 | 1320 | align-items: center; | |
| @@ -1416,66 +1347,68 @@ | |||
| 1416 | 1347 | font-size: 0.85rem; | |
| 1417 | 1348 | } | |
| 1418 | 1349 | ||
| 1419 | - | .insights-body { | |
| 1420 | - | gap: 1.5rem; | |
| 1421 | - | } | |
| 1422 | - | ||
| 1423 | - | .insight-block { | |
| 1424 | - | display: flex; | |
| 1425 | - | flex-direction: column; | |
| 1426 | - | gap: 0.9rem; | |
| 1427 | - | } | |
| 1428 | - | ||
| 1429 | - | .block-title { | |
| 1430 | - | font-size: 0.85rem; | |
| 1431 | - | text-transform: uppercase; | |
| 1432 | - | letter-spacing: 0.08em; | |
| 1433 | - | color: var(--dynamic-muted); | |
| 1434 | - | margin: 0; | |
| 1350 | + | .quick-access-body { | |
| 1351 | + | gap: 1rem; | |
| 1435 | 1352 | } | |
| 1436 | 1353 | ||
| 1437 | - | .insights-list { | |
| 1354 | + | .quick-access-grid-compact { | |
| 1438 | 1355 | display: flex; | |
| 1439 | 1356 | flex-direction: column; | |
| 1440 | - | gap: 0.75rem; | |
| 1357 | + | gap: 0.5rem; | |
| 1358 | + | background-color: var(--dynamic-surface-variant); | |
| 1359 | + | border: 2px solid var(--dynamic-border); | |
| 1360 | + | padding: 0.5rem; | |
| 1441 | 1361 | } | |
| 1442 | 1362 | ||
| 1443 | - | .insight-item { | |
| 1363 | + | .quick-access-item-compact { | |
| 1444 | 1364 | display: flex; | |
| 1445 | 1365 | align-items: center; | |
| 1446 | 1366 | gap: 0.75rem; | |
| 1447 | - | border: 2px solid var(--dynamic-border); | |
| 1367 | + | border-bottom: 1px solid var(--dynamic-border); | |
| 1448 | 1368 | background-color: var(--dynamic-surface-variant); | |
| 1449 | - | padding: 0.85rem 1rem; | |
| 1369 | + | padding: 0.5rem 0.25rem; | |
| 1450 | 1370 | cursor: pointer; | |
| 1451 | - | border-radius: 0; | |
| 1452 | - | transition: | |
| 1453 | - | border-color 200ms ease, | |
| 1454 | - | background-color 200ms ease; | |
| 1371 | + | transition: background-color 200ms ease; | |
| 1455 | 1372 | } | |
| 1456 | 1373 | ||
| 1457 | - | .insight-item:hover, | |
| 1458 | - | .insight-item:focus-visible { | |
| 1459 | - | border-color: var(--dynamic-accent); | |
| 1374 | + | .quick-access-item-compact:last-child { | |
| 1375 | + | border-bottom: none; | |
| 1376 | + | } | |
| 1377 | + | ||
| 1378 | + | .quick-access-item-compact:hover, | |
| 1379 | + | .quick-access-item-compact:focus-visible { | |
| 1380 | + | background-color: var(--dynamic-surface); | |
| 1460 | 1381 | outline: none; | |
| 1461 | 1382 | } | |
| 1462 | 1383 | ||
| 1463 | - | .insight-rank { | |
| 1464 | - | width: 2rem; | |
| 1465 | - | height: 2rem; | |
| 1384 | + | .quick-access-favicon-compact { | |
| 1385 | + | width: 32px; | |
| 1386 | + | height: 32px; | |
| 1466 | 1387 | border: 2px solid var(--dynamic-border); | |
| 1467 | - | background-color: var(--dynamic-surface); | |
| 1388 | + | background-color: var(--dynamic-surface-variant); | |
| 1468 | 1389 | display: flex; | |
| 1469 | 1390 | align-items: center; | |
| 1470 | 1391 | justify-content: center; | |
| 1471 | - | font-weight: 600; | |
| 1472 | - | font-size: 0.9rem; | |
| 1473 | - | color: var(--dynamic-accent); | |
| 1474 | 1392 | flex-shrink: 0; | |
| 1475 | 1393 | border-radius: 0; | |
| 1476 | 1394 | } | |
| 1477 | 1395 | ||
| 1478 | - | .insight-body { | |
| 1396 | + | .quick-access-favicon-img-compact { | |
| 1397 | + | width: 70%; | |
| 1398 | + | height: 70%; | |
| 1399 | + | object-fit: contain; | |
| 1400 | + | } | |
| 1401 | + | ||
| 1402 | + | .quick-access-fallback-compact { | |
| 1403 | + | width: 100%; | |
| 1404 | + | height: 100%; | |
| 1405 | + | display: flex; | |
| 1406 | + | align-items: center; | |
| 1407 | + | justify-content: center; | |
| 1408 | + | color: var(--dynamic-muted); | |
| 1409 | + | } | |
| 1410 | + | ||
| 1411 | + | .quick-access-meta-compact { | |
| 1479 | 1412 | flex: 1; | |
| 1480 | 1413 | min-width: 0; | |
| 1481 | 1414 | display: flex; | |
| @@ -1483,48 +1416,63 @@ | |||
| 1483 | 1416 | gap: 0.125rem; | |
| 1484 | 1417 | } | |
| 1485 | 1418 | ||
| 1486 | - | .insight-title { | |
| 1419 | + | .quick-access-title-compact { | |
| 1420 | + | font-size: 0.95rem; | |
| 1487 | 1421 | font-weight: 600; | |
| 1488 | 1422 | color: var(--dynamic-text); | |
| 1489 | 1423 | white-space: nowrap; | |
| 1490 | 1424 | overflow: hidden; | |
| 1491 | - | text-overflow: ellipsis; | |
| 1425 | + | position: relative; | |
| 1426 | + | mask-image: linear-gradient(to right, black 70%, transparent 100%); | |
| 1427 | + | -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%); | |
| 1492 | 1428 | } | |
| 1493 | 1429 | ||
| 1494 | - | .insight-domain { | |
| 1495 | - | font-size: 0.85rem; | |
| 1430 | + | .quick-access-domain-compact { | |
| 1431 | + | font-size: 0.8rem; | |
| 1496 | 1432 | color: var(--dynamic-muted); | |
| 1497 | 1433 | white-space: nowrap; | |
| 1498 | 1434 | overflow: hidden; | |
| 1499 | 1435 | text-overflow: ellipsis; | |
| 1500 | 1436 | } | |
| 1501 | 1437 | ||
| 1502 | - | .insight-visits { | |
| 1503 | - | font-size: 0.78rem; | |
| 1504 | - | color: var(--dynamic-accent); | |
| 1505 | - | text-transform: uppercase; | |
| 1506 | - | letter-spacing: 0.05em; | |
| 1507 | - | font-weight: 600; | |
| 1438 | + | .quick-access-actions-compact { | |
| 1439 | + | display: flex; | |
| 1440 | + | align-items: center; | |
| 1441 | + | gap: 0.5rem; | |
| 1508 | 1442 | flex-shrink: 0; | |
| 1509 | 1443 | } | |
| 1510 | 1444 | ||
| 1511 | - | .stats-grid { | |
| 1445 | + | .quick-access-pin-compact { | |
| 1512 | 1446 | display: flex; | |
| 1513 | - | flex-wrap: wrap; | |
| 1514 | - | gap: 0.85rem; | |
| 1515 | - | } | |
| 1516 | - | ||
| 1517 | - | .stat-item { | |
| 1518 | - | flex: 1 1 160px; | |
| 1519 | - | border: 2px solid var(--dynamic-border); | |
| 1447 | + | align-items: center; | |
| 1448 | + | justify-content: center; | |
| 1449 | + | width: 24px; | |
| 1450 | + | height: 24px; | |
| 1451 | + | border: 1px solid var(--dynamic-border); | |
| 1520 | 1452 | background-color: var(--dynamic-surface-variant); | |
| 1521 | - | padding: 1rem; | |
| 1522 | - | display: flex; | |
| 1523 | - | flex-direction: column; | |
| 1524 | - | gap: 0.35rem; | |
| 1453 | + | color: var(--dynamic-muted); | |
| 1454 | + | transition: background-color 200ms ease, color 200ms ease; | |
| 1455 | + | cursor: pointer; | |
| 1525 | 1456 | border-radius: 0; | |
| 1526 | 1457 | } | |
| 1527 | 1458 | ||
| 1459 | + | .quick-access-pin-compact:hover, | |
| 1460 | + | .quick-access-pin-compact:focus-visible { | |
| 1461 | + | color: var(--dynamic-accent); | |
| 1462 | + | background-color: var(--dynamic-surface); | |
| 1463 | + | outline: none; | |
| 1464 | + | } | |
| 1465 | + | ||
| 1466 | + | .quick-access-visits-compact { | |
| 1467 | + | font-size: 0.75rem; | |
| 1468 | + | color: var(--dynamic-accent); | |
| 1469 | + | text-transform: uppercase; | |
| 1470 | + | letter-spacing: 0.05em; | |
| 1471 | + | font-weight: 600; | |
| 1472 | + | flex-shrink: 0; | |
| 1473 | + | } | |
| 1474 | + | ||
| 1475 | + | ||
| 1528 | 1476 | .stat-value { | |
| 1529 | 1477 | font-size: 1.4rem; | |
| 1530 | 1478 | font-weight: 600; | |
| @@ -1538,6 +1486,26 @@ | |||
| 1538 | 1486 | letter-spacing: 0.08em; | |
| 1539 | 1487 | } | |
| 1540 | 1488 | ||
| 1489 | + | .stats-grid-horizontal { | |
| 1490 | + | display: flex; | |
| 1491 | + | flex-wrap: wrap; | |
| 1492 | + | gap: 1rem; | |
| 1493 | + | justify-content: space-between; | |
| 1494 | + | } | |
| 1495 | + | ||
| 1496 | + | .stat-item-horizontal { | |
| 1497 | + | flex: 1; | |
| 1498 | + | min-width: 120px; | |
| 1499 | + | border: 2px solid var(--dynamic-border); | |
| 1500 | + | background-color: var(--dynamic-surface-variant); | |
| 1501 | + | padding: 0.75rem; | |
| 1502 | + | display: flex; | |
| 1503 | + | flex-direction: column; | |
| 1504 | + | gap: 0.25rem; | |
| 1505 | + | text-align: center; | |
| 1506 | + | border-radius: 0; | |
| 1507 | + | } | |
| 1508 | + | ||
| 1541 | 1509 | .shortcuts-panel { | |
| 1542 | 1510 | gap: 0; | |
| 1543 | 1511 | } | |
| @@ -1587,7 +1555,7 @@ | |||
| 1587 | 1555 | /* Responsive Design */ | |
| 1588 | 1556 | @media (max-width: 1200px) { | |
| 1589 | 1557 | .history-panel, | |
| 1590 | - | .insights-panel { | |
| 1558 | + | .quick-access-panel { | |
| 1591 | 1559 | min-width: min(28rem, 100%); | |
| 1592 | 1560 | } | |
| 1593 | 1561 | } | |
| @@ -1602,13 +1570,12 @@ | |||
| 1602 | 1570 | } | |
| 1603 | 1571 | ||
| 1604 | 1572 | .history-panel, | |
| 1605 | - | .insights-panel { | |
| 1573 | + | .quick-access-panel { | |
| 1606 | 1574 | min-width: 100%; | |
| 1575 | + | max-width: 100%; | |
| 1576 | + | width: 100%; | |
| 1607 | 1577 | } | |
| 1608 | 1578 | ||
| 1609 | - | .quick-access-item { | |
| 1610 | - | flex: 1 1 calc(50% - 1rem); | |
| 1611 | - | } | |
| 1612 | 1579 | } | |
| 1613 | 1580 | ||
| 1614 | 1581 | @media (max-width: 640px) { | |
| @@ -1618,29 +1585,33 @@ | |||
| 1618 | 1585 | } | |
| 1619 | 1586 | ||
| 1620 | 1587 | .panel-body, | |
| 1621 | - | .panel-header, | |
| 1622 | - | .hero-panel { | |
| 1588 | + | .panel-header { | |
| 1623 | 1589 | padding-left: 1.25rem; | |
| 1624 | 1590 | padding-right: 1.25rem; | |
| 1625 | 1591 | } | |
| 1626 | 1592 | ||
| 1627 | - | .quick-access-item { | |
| 1628 | - | flex: 1 1 100%; | |
| 1629 | - | } | |
| 1630 | 1593 | ||
| 1631 | 1594 | .history-item { | |
| 1632 | 1595 | flex-direction: column; | |
| 1633 | 1596 | align-items: flex-start; | |
| 1634 | 1597 | } | |
| 1635 | 1598 | ||
| 1636 | - | .history-delete { | |
| 1599 | + | .history-actions { | |
| 1637 | 1600 | width: 100%; | |
| 1638 | - | border-left: 0; | |
| 1639 | 1601 | border-top: 2px solid var(--dynamic-border); | |
| 1640 | - | align-self: stretch; | |
| 1641 | 1602 | } | |
| 1642 | 1603 | ||
| 1643 | - | .history-item-top { | |
| 1604 | + | .history-pin, | |
| 1605 | + | .history-delete { | |
| 1606 | + | flex: 1; | |
| 1607 | + | border-left: 0; | |
| 1608 | + | } | |
| 1609 | + | ||
| 1610 | + | .history-pin { | |
| 1611 | + | border-right: 1px solid var(--dynamic-border); | |
| 1612 | + | } | |
| 1613 | + | ||
| 1614 | + | .history-item-main { | |
| 1644 | 1615 | flex-direction: column; | |
| 1645 | 1616 | align-items: flex-start; | |
| 1646 | 1617 | gap: 0.35rem; | |
| @@ -1649,5 +1620,37 @@ | |||
| 1649 | 1620 | .history-time { | |
| 1650 | 1621 | order: 3; | |
| 1651 | 1622 | } | |
| 1623 | + | ||
| 1624 | + | .stats-grid-horizontal { | |
| 1625 | + | flex-direction: column; | |
| 1626 | + | gap: 0.75rem; | |
| 1627 | + | } | |
| 1628 | + | ||
| 1629 | + | .stat-item-horizontal { | |
| 1630 | + | min-width: 100%; | |
| 1631 | + | } | |
| 1632 | + | } | |
| 1633 | + | ||
| 1634 | + | .show-more-button { | |
| 1635 | + | width: 100%; | |
| 1636 | + | padding: 0.75rem 1rem; | |
| 1637 | + | border: 2px solid var(--dynamic-border); | |
| 1638 | + | background-color: var(--dynamic-surface-variant); | |
| 1639 | + | color: var(--dynamic-text); | |
| 1640 | + | font-size: 0.9rem; | |
| 1641 | + | font-weight: 500; | |
| 1642 | + | cursor: pointer; | |
| 1643 | + | transition: | |
| 1644 | + | background-color 200ms ease, | |
| 1645 | + | border-color 200ms ease; | |
| 1646 | + | border-radius: 0; | |
| 1647 | + | margin-top: 0.5rem; | |
| 1648 | + | } | |
| 1649 | + | ||
| 1650 | + | .show-more-button:hover, | |
| 1651 | + | .show-more-button:focus-visible { | |
| 1652 | + | background-color: var(--dynamic-surface); | |
| 1653 | + | border-color: var(--dynamic-accent); | |
| 1654 | + | outline: none; | |
| 1652 | 1655 | } | |
| 1653 | 1656 | </style> | |
| @@ -38,54 +38,57 @@ | |||||
| 38 | let historyOffset = $state(0); | 38 | let historyOffset = $state(0); | ||
| 39 | let hasMoreHistory = $state(true); | 39 | let hasMoreHistory = $state(true); | ||
| 40 | let loadingMoreHistory = $state(false); | 40 | let loadingMoreHistory = $state(false); | ||
| 41 | - | const MAX_QUICK_ACCESS_ITEMS = 9; | 41 | + | const MIN_VISIT_COUNT_FOR_QUICK_ACCESS = 2; |
| 42 | type ThemeMode = "light" | "dark"; | 42 | type ThemeMode = "light" | "dark"; | ||
| 43 | const THEME_STORAGE_KEY = "dumber.theme"; | 43 | const THEME_STORAGE_KEY = "dumber.theme"; | ||
| 44 | let themeMode = $state<ThemeMode>("dark"); | 44 | let themeMode = $state<ThemeMode>("dark"); | ||
| 45 | let themeObserver: MutationObserver | null = null; | 45 | let themeObserver: MutationObserver | null = null; | ||
| 46 | - | let historyHeaderRef = $state<HTMLElement | null>(null); | 46 | + | const INITIAL_HISTORY_DISPLAY = 15; |
| 47 | - | let historyBodyRef = $state<HTMLElement | null>(null); | 47 | + | let showAllHistory = $state(false); |
| 48 | - | let insightsPanelRef = $state<HTMLElement | null>(null); | 48 | + | const PINNED_SITES_STORAGE_KEY = "dumber.pinnedSites"; |
| 49 | - | let resizeSyncRaf = 0; | 49 | + | let pinnedSites = $state<Set<string>>(new Set()); |
| 50 | + | let displayedHistoryItems = $derived( | |||
| 51 | + | showAllHistory ? historyItems : historyItems.slice(0, INITIAL_HISTORY_DISPLAY) | |||
| 52 | + | ); | |||
| 50 | 53 | ||||
| 51 | - | const syncHistoryPanelSize = () => { | 54 | + | // Function for checking if an item is being deleted |
| 52 | - | if (!historyBodyRef || !historyHeaderRef || !insightsPanelRef) return; | 55 | + | const isDeleting = (id: number) => deletingIds.includes(id); |
| 53 | 56 | ||||
| 54 | - | if (window.innerWidth <= 960) { | 57 | + | // Pinned sites management |
| 55 | - | historyBodyRef.style.maxHeight = ""; | 58 | + | const loadPinnedSites = () => { |
| 56 | - | return; | 59 | + | try { |
| 60 | + | const stored = localStorage.getItem(PINNED_SITES_STORAGE_KEY); | |||
| 61 | + | if (stored) { | |||
| 62 | + | const urls = JSON.parse(stored) as string[]; | |||
| 63 | + | pinnedSites = new Set(urls); | |||
| 64 | + | } | |||
| 65 | + | } catch (error) { | |||
| 66 | + | console.warn("[homepage] Failed to load pinned sites", error); | |||
| 67 | + | pinnedSites = new Set(); | |||
| 57 | } | 68 | } | ||
| 69 | + | }; | |||
| 58 | 70 | ||||
| 59 | - | const insightsHeight = insightsPanelRef.getBoundingClientRect().height; | 71 | + | const savePinnedSites = () => { |
| 60 | - | if (!insightsHeight || Number.isNaN(insightsHeight)) { | 72 | + | try { |
| 61 | - | historyBodyRef.style.maxHeight = ""; | 73 | + | const urls = Array.from(pinnedSites); |
| 62 | - | return; | 74 | + | localStorage.setItem(PINNED_SITES_STORAGE_KEY, JSON.stringify(urls)); |
| 75 | + | } catch (error) { | |||
| 76 | + | console.warn("[homepage] Failed to save pinned sites", error); | |||
| 63 | } | 77 | } | ||
| 78 | + | }; | |||
| 64 | 79 | ||||
| 65 | - | const headerHeight = historyHeaderRef.getBoundingClientRect().height; | 80 | + | const isPinned = (url: string): boolean => pinnedSites.has(url); |
| 66 | - | const bodyStyles = getComputedStyle(historyBodyRef); | |||
| 67 | - | const paddingTop = parseFloat(bodyStyles.paddingTop) || 0; | |||
| 68 | - | const paddingBottom = parseFloat(bodyStyles.paddingBottom) || 0; | |||
| 69 | - | const maxHeight = insightsHeight - headerHeight - paddingTop - paddingBottom; | |||
| 70 | 81 | ||||
| 71 | - | if (maxHeight > 0) { | 82 | + | const togglePin = (url: string) => { |
| 72 | - | historyBodyRef.style.maxHeight = `${maxHeight}px`; | 83 | + | if (pinnedSites.has(url)) { |
| 84 | + | pinnedSites.delete(url); | |||
| 73 | } else { | 85 | } else { | ||
| 74 | - | historyBodyRef.style.maxHeight = ""; | 86 | + | pinnedSites.add(url); |
| 75 | } | 87 | } | ||
| 88 | + | pinnedSites = new Set(pinnedSites); // Trigger reactivity | |||
| 89 | + | savePinnedSites(); | |||
| 76 | }; | 90 | }; | ||
| 77 | 91 | ||||
| 78 | - | const scheduleHistoryPanelSync = () => { | |||
| 79 | - | if (resizeSyncRaf) cancelAnimationFrame(resizeSyncRaf); | |||
| 80 | - | resizeSyncRaf = requestAnimationFrame(() => { | |||
| 81 | - | resizeSyncRaf = 0; | |||
| 82 | - | syncHistoryPanelSize(); | |||
| 83 | - | }); | |||
| 84 | - | }; | |||
| 85 | - | ||||
| 86 | - | // Function for checking if an item is being deleted | |||
| 87 | - | const isDeleting = (id: number) => deletingIds.includes(id); | |||
| 88 | - | ||||
| 89 | // Initialize shortcuts (these are browser shortcuts, not dynamic from API) | 92 | // Initialize shortcuts (these are browser shortcuts, not dynamic from API) | ||
| 90 | const initializeShortcuts = () => { | 93 | const initializeShortcuts = () => { | ||
| 91 | shortcuts = [ | 94 | shortcuts = [ | ||
| @@ -396,6 +399,23 @@ | |||||
| 396 | return domain || item.url; | 399 | return domain || item.url; | ||
| 397 | }; | 400 | }; | ||
| 398 | 401 | ||||
| 402 | + | const highlightDomainInUrl = (url: string): string => { | |||
| 403 | + | try { | |||
| 404 | + | const urlObj = new URL(url); | |||
| 405 | + | const domain = urlObj.hostname; | |||
| 406 | + | const domainStart = url.indexOf(domain); | |||
| 407 | + | ||||
| 408 | + | if (domainStart === -1) return url; | |||
| 409 | + | ||||
| 410 | + | const beforeDomain = url.slice(0, domainStart); | |||
| 411 | + | const afterDomain = url.slice(domainStart + domain.length); | |||
| 412 | + | ||||
| 413 | + | return `${beforeDomain}<span class="history-url-domain">${domain}</span>${afterDomain}`; | |||
| 414 | + | } catch { | |||
| 415 | + | return url; | |||
| 416 | + | } | |||
| 417 | + | }; | |||
| 418 | + | ||||
| 399 | // Format relative time | 419 | // Format relative time | ||
| 400 | const formatTime = (timestamp: string): string => { | 420 | const formatTime = (timestamp: string): string => { | ||
| 401 | try { | 421 | try { | ||
| @@ -444,27 +464,42 @@ | |||||
| 444 | 464 | ||||
| 445 | const pushItem = (item: HistoryItem | null | undefined) => { | 465 | const pushItem = (item: HistoryItem | null | undefined) => { | ||
| 446 | if (!item) return; | 466 | if (!item) return; | ||
| 447 | - | const domain = getDomain(item.url); | 467 | + | if (seen.has(item.url)) return; |
| 448 | - | const key = domain || item.url; | 468 | + | seen.add(item.url); |
| 449 | - | if (seen.has(key)) return; | |||
| 450 | - | seen.add(key); | |||
| 451 | picks.push(item); | 469 | picks.push(item); | ||
| 452 | }; | 470 | }; | ||
| 453 | 471 | ||||
| 472 | + | // First, add pinned sites (no visit count requirement for pinned) | |||
| 454 | for (const item of topVisited ?? []) { | 473 | for (const item of topVisited ?? []) { | ||
| 455 | - | pushItem(item); | 474 | + | if (pinnedSites.has(item.url)) { |
| 456 | - | if (picks.length >= MAX_QUICK_ACCESS_ITEMS) return picks; | 475 | + | pushItem(item); |
| 476 | + | } | |||
| 457 | } | 477 | } | ||
| 458 | 478 | ||||
| 459 | for (const item of historyItems ?? []) { | 479 | for (const item of historyItems ?? []) { | ||
| 460 | - | pushItem(item); | 480 | + | if (pinnedSites.has(item.url)) { |
| 461 | - | if (picks.length >= MAX_QUICK_ACCESS_ITEMS) break; | 481 | + | pushItem(item); |
| 482 | + | } | |||
| 483 | + | } | |||
| 484 | + | ||||
| 485 | + | // Then add most visited sites (non-pinned, with minimum visit count) | |||
| 486 | + | for (const item of topVisited ?? []) { | |||
| 487 | + | if (!pinnedSites.has(item.url) && item.visit_count >= MIN_VISIT_COUNT_FOR_QUICK_ACCESS) { | |||
| 488 | + | pushItem(item); | |||
| 489 | + | } | |||
| 490 | + | } | |||
| 491 | + | ||||
| 492 | + | // Finally add recent history (non-pinned, with minimum visit count) | |||
| 493 | + | for (const item of historyItems ?? []) { | |||
| 494 | + | if (!pinnedSites.has(item.url) && item.visit_count >= MIN_VISIT_COUNT_FOR_QUICK_ACCESS) { | |||
| 495 | + | pushItem(item); | |||
| 496 | + | } | |||
| 462 | } | 497 | } | ||
| 463 | 498 | ||||
| 464 | return picks; | 499 | return picks; | ||
| 465 | }; | 500 | }; | ||
| 466 | 501 | ||||
| 467 | - | let quickAccess = $derived<HistoryItem[]>(buildQuickAccess()); | 502 | + | let quickAccess = $derived<HistoryItem[]>((pinnedSites.size, buildQuickAccess())); |
| 468 | let quickAccessLoading = $derived(topVisitedLoading || historyLoading); | 503 | let quickAccessLoading = $derived(topVisitedLoading || historyLoading); | ||
| 469 | 504 | ||||
| 470 | const persistTheme = (mode: ThemeMode) => { | 505 | const persistTheme = (mode: ThemeMode) => { | ||
| @@ -598,6 +633,7 @@ | |||||
| 598 | }; | 633 | }; | ||
| 599 | 634 | ||||
| 600 | onMount(() => { | 635 | onMount(() => { | ||
| 636 | + | loadPinnedSites(); | |||
| 601 | initializeShortcuts(); | 637 | initializeShortcuts(); | ||
| 602 | fetchHistory().then(() => { | 638 | fetchHistory().then(() => { | ||
| 603 | // Setup infinite scroll after initial history is loaded | 639 | // Setup infinite scroll after initial history is loaded | ||
| @@ -619,33 +655,12 @@ | |||||
| 619 | attributeFilter: ["class"], | 655 | attributeFilter: ["class"], | ||
| 620 | }); | 656 | }); | ||
| 621 | 657 | ||||
| 622 | - | window.addEventListener('resize', scheduleHistoryPanelSync); | |||
| 623 | - | ||||
| 624 | return () => { | 658 | return () => { | ||
| 625 | themeObserver?.disconnect(); | 659 | themeObserver?.disconnect(); | ||
| 626 | themeObserver = null; | 660 | themeObserver = null; | ||
| 627 | - | window.removeEventListener('resize', scheduleHistoryPanelSync); | |||
| 628 | - | if (resizeSyncRaf) { | |||
| 629 | - | cancelAnimationFrame(resizeSyncRaf); | |||
| 630 | - | resizeSyncRaf = 0; | |||
| 631 | - | } | |||
| 632 | }; | 661 | }; | ||
| 633 | }); | 662 | }); | ||
| 634 | 663 | ||||
| 635 | - | $effect(() => { | |||
| 636 | - | historyItems; | |||
| 637 | - | topVisited; | |||
| 638 | - | stats; | |||
| 639 | - | shortcuts; | |||
| 640 | - | scheduleHistoryPanelSync(); | |||
| 641 | - | }); | |||
| 642 | - | ||||
| 643 | - | $effect(() => { | |||
| 644 | - | historyHeaderRef; | |||
| 645 | - | historyBodyRef; | |||
| 646 | - | insightsPanelRef; | |||
| 647 | - | scheduleHistoryPanelSync(); | |||
| 648 | - | }); | |||
| 649 | 664 | ||||
| 650 | </script> | 665 | </script> | ||
| 651 | 666 | ||||
| @@ -691,86 +706,13 @@ | |||||
| 691 | {/if} | 706 | {/if} | ||
| 692 | </button> | 707 | </button> | ||
| 693 | </div> | 708 | </div> | ||
| 694 | - | ||||
| 695 | - | <section class="hero-panel brutal-panel"> | |||
| 696 | - | <div class="hero-heading"> | |||
| 697 | - | <h1 class="hero-title">Jump back in</h1> | |||
| 698 | - | <p class="hero-subtitle">Direct access to the sites you visit the most, plus what you just explored.</p> | |||
| 699 | - | </div> | |||
| 700 | - | <div class="quick-access-wrapper"> | |||
| 701 | - | {#if quickAccessLoading} | |||
| 702 | - | <div class="loading quick-access-loading">Preparing your shortcuts...</div> | |||
| 703 | - | {:else if quickAccess.length === 0} | |||
| 704 | - | <div class="empty-state quick-access-empty"> | |||
| 705 | - | <h3>No quick links yet</h3> | |||
| 706 | - | <p>Browse a few sites and we’ll surface your frequent destinations here.</p> | |||
| 707 | - | </div> | |||
| 708 | - | {:else} | |||
| 709 | - | <div class="quick-access-grid"> | |||
| 710 | - | {#each quickAccess as item (item.id)} | |||
| 711 | - | <div | |||
| 712 | - | class="quick-access-item" | |||
| 713 | - | role="button" | |||
| 714 | - | tabindex="0" | |||
| 715 | - | onclick={() => navigateTo(item.url)} | |||
| 716 | - | onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)} | |||
| 717 | - | > | |||
| 718 | - | <div class="quick-access-favicon"> | |||
| 719 | - | {#if item.favicon_url} | |||
| 720 | - | <img | |||
| 721 | - | src={item.favicon_url} | |||
| 722 | - | alt="" | |||
| 723 | - | class="quick-access-favicon-img" | |||
| 724 | - | onerror={(e) => { | |||
| 725 | - | const target = e.target as HTMLImageElement; | |||
| 726 | - | if (target) { | |||
| 727 | - | target.style.display = 'none'; | |||
| 728 | - | const fallback = target.nextElementSibling as HTMLElement; | |||
| 729 | - | if (fallback) fallback.style.display = 'flex'; | |||
| 730 | - | } | |||
| 731 | - | }} | |||
| 732 | - | onload={(e) => { | |||
| 733 | - | const target = e.target as HTMLImageElement; | |||
| 734 | - | if (target) { | |||
| 735 | - | const fallback = target.nextElementSibling as HTMLElement; | |||
| 736 | - | if (fallback) fallback.style.display = 'none'; | |||
| 737 | - | } | |||
| 738 | - | }} | |||
| 739 | - | /> | |||
| 740 | - | <div class="quick-access-fallback" style="display: none;"> | |||
| 741 | - | <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |||
| 742 | - | <circle cx="12" cy="12" r="10" /> | |||
| 743 | - | <path d="M2 12h20" /> | |||
| 744 | - | </svg> | |||
| 745 | - | </div> | |||
| 746 | - | {:else} | |||
| 747 | - | <div class="quick-access-fallback"> | |||
| 748 | - | <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |||
| 749 | - | <circle cx="12" cy="12" r="10" /> | |||
| 750 | - | <path d="M2 12h20" /> | |||
| 751 | - | </svg> | |||
| 752 | - | </div> | |||
| 753 | - | {/if} | |||
| 754 | - | </div> | |||
| 755 | - | <div class="quick-access-meta"> | |||
| 756 | - | <div class="quick-access-title">{getDisplayTitle(item)}</div> | |||
| 757 | - | <div class="quick-access-domain">{getDomain(item.url)}</div> | |||
| 758 | - | </div> | |||
| 759 | - | <div class="quick-access-visits">{formatVisitLabel(item.visit_count)}</div> | |||
| 760 | - | </div> | |||
| 761 | - | {/each} | |||
| 762 | - | </div> | |||
| 763 | - | {/if} | |||
| 764 | - | </div> | |||
| 765 | - | </section> | |||
| 766 | - | ||||
| 767 | <div class="main-panels"> | 709 | <div class="main-panels"> | ||
| 768 | <section class="history-panel brutal-panel"> | 710 | <section class="history-panel brutal-panel"> | ||
| 769 | - | <div class="panel-header" bind:this={historyHeaderRef}> | 711 | + | <div class="panel-header"> |
| 770 | <h2 class="panel-title">Recent History</h2> | 712 | <h2 class="panel-title">Recent History</h2> | ||
| 771 | <p class="panel-subtitle">Scroll to revisit anything from your latest sessions.</p> | 713 | <p class="panel-subtitle">Scroll to revisit anything from your latest sessions.</p> | ||
| 772 | </div> | 714 | </div> | ||
| 773 | - | <div class="panel-body history-body" bind:this={historyBodyRef}> | 715 | + | <div class="panel-body history-body"> |
| 774 | {#if historyLoading} | 716 | {#if historyLoading} | ||
| 775 | <div class="loading">Loading history...</div> | 717 | <div class="loading">Loading history...</div> | ||
| 776 | {:else if historyItems.length === 0} | 718 | {:else if historyItems.length === 0} | ||
| @@ -780,7 +722,7 @@ | |||||
| 780 | </div> | 722 | </div> | ||
| 781 | {:else} | 723 | {:else} | ||
| 782 | <div class="history-list"> | 724 | <div class="history-list"> | ||
| 783 | - | {#each historyItems as item (item.id)} | 725 | + | {#each displayedHistoryItems as item (item.id)} |
| 784 | <div | 726 | <div | ||
| 785 | class="history-item" | 727 | class="history-item" | ||
| 786 | class:deleting={isDeleting(item.id)} | 728 | class:deleting={isDeleting(item.id)} | ||
| @@ -827,29 +769,54 @@ | |||||
| 827 | {/if} | 769 | {/if} | ||
| 828 | </div> | 770 | </div> | ||
| 829 | <div class="history-item-content"> | 771 | <div class="history-item-content"> | ||
| 830 | - | <div class="history-item-top"> | 772 | + | <div class="history-item-main"> |
| 831 | <div class="history-title">{getDisplayTitle(item)}</div> | 773 | <div class="history-title">{getDisplayTitle(item)}</div> | ||
| 832 | <div class="history-time">{formatTime(item.last_visited)}</div> | 774 | <div class="history-time">{formatTime(item.last_visited)}</div> | ||
| 833 | </div> | 775 | </div> | ||
| 834 | - | <div class="history-item-bottom"> | 776 | + | <div class="history-url">{@html highlightDomainInUrl(item.url)}</div> |
| 835 | - | <div class="history-domain">{getDomain(item.url)}</div> | |||
| 836 | - | <div class="history-url">{item.url}</div> | |||
| 837 | - | </div> | |||
| 838 | </div> | 777 | </div> | ||
| 839 | - | <div | 778 | + | <div class="history-actions"> |
| 840 | - | class="history-delete" | 779 | + | <div |
| 841 | - | role="button" | 780 | + | class="history-pin" |
| 842 | - | tabindex="0" | 781 | + | role="button" |
| 843 | - | onclick={(e) => deleteHistoryEntry(item.id, e)} | 782 | + | tabindex="0" |
| 844 | - | onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)} | 783 | + | onclick={(e) => { e.stopPropagation(); togglePin(item.url); }} |
| 845 | - | title="Delete this entry" | 784 | + | onkeydown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); togglePin(item.url); } }} |
| 846 | - | > | 785 | + | title={isPinned(item.url) ? "Unpin from favorites" : "Pin to favorites"} |
| 847 | - | <span aria-hidden="true">×</span> | 786 | + | > |
| 787 | + | {#if isPinned(item.url)} | |||
| 788 | + | <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> | |||
| 789 | + | <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> | |||
| 790 | + | </svg> | |||
| 791 | + | {:else} | |||
| 792 | + | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"> | |||
| 793 | + | <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> | |||
| 794 | + | </svg> | |||
| 795 | + | {/if} | |||
| 796 | + | </div> | |||
| 797 | + | <div | |||
| 798 | + | class="history-delete" | |||
| 799 | + | role="button" | |||
| 800 | + | tabindex="0" | |||
| 801 | + | onclick={(e) => deleteHistoryEntry(item.id, e)} | |||
| 802 | + | onkeydown={(e) => e.key === 'Enter' && deleteHistoryEntry(item.id, e)} | |||
| 803 | + | title="Delete this entry" | |||
| 804 | + | > | |||
| 805 | + | <span aria-hidden="true">×</span> | |||
| 806 | + | </div> | |||
| 848 | </div> | 807 | </div> | ||
| 849 | </div> | 808 | </div> | ||
| 850 | {/each} | 809 | {/each} | ||
| 851 | 810 | ||||
| 852 | - | {#if hasMoreHistory} | 811 | + | {#if !showAllHistory && historyItems.length > INITIAL_HISTORY_DISPLAY} |
| 812 | + | <button | |||
| 813 | + | class="show-more-button" | |||
| 814 | + | onclick={() => showAllHistory = true} | |||
| 815 | + | type="button" | |||
| 816 | + | > | |||
| 817 | + | Show {historyItems.length - INITIAL_HISTORY_DISPLAY} more items | |||
| 818 | + | </button> | |||
| 819 | + | {:else if showAllHistory && hasMoreHistory} | |||
| 853 | <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div> | 820 | <div bind:this={historyScrollSentinel} class="scroll-sentinel"></div> | ||
| 854 | {#if loadingMoreHistory} | 821 | {#if loadingMoreHistory} | ||
| 855 | <div class="loading-more">Loading more history...</div> | 822 | <div class="loading-more">Loading more history...</div> | ||
| @@ -860,75 +827,95 @@ | |||||
| 860 | </div> | 827 | </div> | ||
| 861 | </section> | 828 | </section> | ||
| 862 | 829 | ||||
| 863 | - | <section class="insights-panel brutal-panel" bind:this={insightsPanelRef}> | 830 | + | <section class="quick-access-panel brutal-panel"> |
| 864 | <div class="panel-header"> | 831 | <div class="panel-header"> | ||
| 865 | - | <h2 class="panel-title">Usage Insights</h2> | 832 | + | <h2 class="panel-title">Jump back in</h2> |
| 866 | - | <p class="panel-subtitle">Your browsing patterns at a glance.</p> | 833 | + | <p class="panel-subtitle">Direct access to the sites you like the most.</p> |
| 867 | </div> | 834 | </div> | ||
| 868 | - | <div class="panel-body insights-body"> | 835 | + | <div class="panel-body quick-access-body"> |
| 869 | - | <div class="insight-block"> | 836 | + | {#if quickAccessLoading} |
| 870 | - | <h3 class="block-title">Top sites</h3> | 837 | + | <div class="loading">Preparing your shortcuts...</div> |
| 871 | - | {#if topVisitedLoading} | 838 | + | {:else if quickAccess.length === 0} |
| 872 | - | <div class="loading">Loading usage insights...</div> | 839 | + | <div class="empty-state compact"> |
| 873 | - | {:else if topVisited.length === 0} | 840 | + | <h3>No quick links yet</h3> |
| 874 | - | <div class="empty-state compact"> | 841 | + | <p>Browse a few sites and we'll surface your frequent destinations here.</p> |
| 875 | - | <p>No visit data available</p> | 842 | + | </div> |
| 876 | - | </div> | 843 | + | {:else} |
| 877 | - | {:else} | 844 | + | <div class="quick-access-grid-compact"> |
| 878 | - | <div class="insights-list"> | 845 | + | {#each quickAccess as item (item.id)} |
| 879 | - | {#each topVisited as item, index (item.id)} | 846 | + | <div |
| 847 | + | class="quick-access-item-compact" | |||
| 848 | + | role="button" | |||
| 849 | + | tabindex="0" | |||
| 850 | + | onclick={() => navigateTo(item.url)} | |||
| 851 | + | onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)} | |||
| 852 | + | > | |||
| 853 | + | <div class="quick-access-favicon-compact"> | |||
| 854 | + | {#if item.favicon_url} | |||
| 855 | + | <img | |||
| 856 | + | src={item.favicon_url} | |||
| 857 | + | alt="" | |||
| 858 | + | class="quick-access-favicon-img-compact" | |||
| 859 | + | onerror={(e) => { | |||
| 860 | + | const target = e.target as HTMLImageElement; | |||
| 861 | + | if (target) { | |||
| 862 | + | target.style.display = 'none'; | |||
| 863 | + | const fallback = target.nextElementSibling as HTMLElement; | |||
| 864 | + | if (fallback) fallback.style.display = 'flex'; | |||
| 865 | + | } | |||
| 866 | + | }} | |||
| 867 | + | onload={(e) => { | |||
| 868 | + | const target = e.target as HTMLImageElement; | |||
| 869 | + | if (target) { | |||
| 870 | + | const fallback = target.nextElementSibling as HTMLElement; | |||
| 871 | + | if (fallback) fallback.style.display = 'none'; | |||
| 872 | + | } | |||
| 873 | + | }} | |||
| 874 | + | /> | |||
| 875 | + | <div class="quick-access-fallback-compact" style="display: none;"> | |||
| 876 | + | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |||
| 877 | + | <circle cx="12" cy="12" r="10" /> | |||
| 878 | + | <path d="M2 12h20" /> | |||
| 879 | + | </svg> | |||
| 880 | + | </div> | |||
| 881 | + | {:else} | |||
| 882 | + | <div class="quick-access-fallback-compact"> | |||
| 883 | + | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |||
| 884 | + | <circle cx="12" cy="12" r="10" /> | |||
| 885 | + | <path d="M2 12h20" /> | |||
| 886 | + | </svg> | |||
| 887 | + | </div> | |||
| 888 | + | {/if} | |||
| 889 | + | </div> | |||
| 890 | + | <div class="quick-access-meta-compact"> | |||
| 891 | + | <div class="quick-access-title-compact">{getDisplayTitle(item)}</div> | |||
| 892 | + | <div class="quick-access-domain-compact">{getDomain(item.url)}</div> | |||
| 893 | + | </div> | |||
| 894 | + | <div class="quick-access-actions-compact"> | |||
| 880 | <div | 895 | <div | ||
| 881 | - | class="insight-item" | 896 | + | class="quick-access-pin-compact" |
| 882 | role="button" | 897 | role="button" | ||
| 883 | tabindex="0" | 898 | tabindex="0" | ||
| 884 | - | onclick={() => navigateTo(item.url)} | 899 | + | onclick={(e) => { e.stopPropagation(); togglePin(item.url); }} |
| 885 | - | onkeydown={(e) => e.key === 'Enter' && navigateTo(item.url)} | 900 | + | onkeydown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); togglePin(item.url); } }} |
| 901 | + | title={isPinned(item.url) ? "Unpin from favorites" : "Pin to favorites"} | |||
| 886 | > | 902 | > | ||
| 887 | - | <div class="insight-rank">{index + 1}</div> | 903 | + | {#if isPinned(item.url)} |
| 888 | - | <div class="insight-body"> | 904 | + | <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> |
| 889 | - | <div class="insight-title">{getDisplayTitle(item)}</div> | 905 | + | <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> |
| 890 | - | <div class="insight-domain">{getDomain(item.url)}</div> | 906 | + | </svg> |
| 891 | - | </div> | 907 | + | {:else} |
| 892 | - | <div class="insight-visits">{formatVisitLabel(item.visit_count)}</div> | 908 | + | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"> |
| 909 | + | <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> | |||
| 910 | + | </svg> | |||
| 911 | + | {/if} | |||
| 893 | </div> | 912 | </div> | ||
| 894 | - | {/each} | 913 | + | <div class="quick-access-visits-compact">{formatVisitLabel(item.visit_count)}</div> |
| 895 | - | </div> | |||
| 896 | - | {/if} | |||
| 897 | - | </div> | |||
| 898 | - | ||||
| 899 | - | <div class="insight-block"> | |||
| 900 | - | <h3 class="block-title">History snapshot</h3> | |||
| 901 | - | {#if statsLoading} | |||
| 902 | - | <div class="loading">Loading stats...</div> | |||
| 903 | - | {:else if stats} | |||
| 904 | - | <div class="stats-grid"> | |||
| 905 | - | <div class="stat-item"> | |||
| 906 | - | <div class="stat-value">{stats.total_entries}</div> | |||
| 907 | - | <div class="stat-label">Entries Stored</div> | |||
| 908 | </div> | 914 | </div> | ||
| 909 | - | <div class="stat-item"> | |||
| 910 | - | <div class="stat-value">{stats.recent_count}</div> | |||
| 911 | - | <div class="stat-label">Recent Window</div> | |||
| 912 | - | </div> | |||
| 913 | - | {#if stats.newest_entry} | |||
| 914 | - | <div class="stat-item"> | |||
| 915 | - | <div class="stat-value">{formatTime(stats.newest_entry)}</div> | |||
| 916 | - | <div class="stat-label">Newest Visit</div> | |||
| 917 | - | </div> | |||
| 918 | - | {/if} | |||
| 919 | - | {#if stats.oldest_entry} | |||
| 920 | - | <div class="stat-item"> | |||
| 921 | - | <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div> | |||
| 922 | - | <div class="stat-label">Oldest Entry Loaded</div> | |||
| 923 | - | </div> | |||
| 924 | - | {/if} | |||
| 925 | - | </div> | |||
| 926 | - | {:else} | |||
| 927 | - | <div class="empty-state compact"> | |||
| 928 | - | <p>No statistics available</p> | |||
| 929 | </div> | 915 | </div> | ||
| 930 | - | {/if} | 916 | + | {/each} |
| 931 | </div> | 917 | </div> | ||
| 918 | + | {/if} | |||
| 932 | </div> | 919 | </div> | ||
| 933 | </section> | 920 | </section> | ||
| 934 | </div> | 921 | </div> | ||
| @@ -953,6 +940,47 @@ | |||||
| 953 | {/if} | 940 | {/if} | ||
| 954 | </div> | 941 | </div> | ||
| 955 | </section> | 942 | </section> | ||
| 943 | + | ||||
| 944 | + | ||||
| 945 | + | <section class="stats-panel brutal-panel"> | |||
| 946 | + | <div class="panel-header"> | |||
| 947 | + | <h2 class="panel-title">Usage Stats</h2> | |||
| 948 | + | <p class="panel-subtitle">Your browsing activity at a glance.</p> | |||
| 949 | + | </div> | |||
| 950 | + | <div class="panel-body"> | |||
| 951 | + | {#if statsLoading} | |||
| 952 | + | <div class="loading">Loading stats...</div> | |||
| 953 | + | {:else if stats} | |||
| 954 | + | <div class="stats-grid-horizontal"> | |||
| 955 | + | <div class="stat-item-horizontal"> | |||
| 956 | + | <div class="stat-value">{stats.total_entries}</div> | |||
| 957 | + | <div class="stat-label">Entries Stored</div> | |||
| 958 | + | </div> | |||
| 959 | + | <div class="stat-item-horizontal"> | |||
| 960 | + | <div class="stat-value">{stats.recent_count}</div> | |||
| 961 | + | <div class="stat-label">Recent Window</div> | |||
| 962 | + | </div> | |||
| 963 | + | {#if stats.newest_entry} | |||
| 964 | + | <div class="stat-item-horizontal"> | |||
| 965 | + | <div class="stat-value">{formatTime(stats.newest_entry)}</div> | |||
| 966 | + | <div class="stat-label">Newest Visit</div> | |||
| 967 | + | </div> | |||
| 968 | + | {/if} | |||
| 969 | + | {#if stats.oldest_entry} | |||
| 970 | + | <div class="stat-item-horizontal"> | |||
| 971 | + | <div class="stat-value">{formatCalendarDate(stats.oldest_entry)}</div> | |||
| 972 | + | <div class="stat-label">Oldest Entry</div> | |||
| 973 | + | </div> | |||
| 974 | + | {/if} | |||
| 975 | + | </div> | |||
| 976 | + | {:else} | |||
| 977 | + | <div class="empty-state compact"> | |||
| 978 | + | <p>No statistics available</p> | |||
| 979 | + | </div> | |||
| 980 | + | {/if} | |||
| 981 | + | </div> | |||
| 982 | + | </section> | |||
| 983 | + | ||||
| 956 | </div> | 984 | </div> | ||
| 957 | 985 | ||||
| 958 | <Footer /> | 986 | <Footer /> | ||
| @@ -1039,126 +1067,7 @@ | |||||
| 1039 | gap: 0; | 1067 | gap: 0; | ||
| 1040 | } | 1068 | } | ||
| 1041 | 1069 | ||||
| 1042 | - | .hero-panel { | |||
| 1043 | - | gap: clamp(1.25rem, 2vw, 1.75rem); | |||
| 1044 | - | padding: clamp(1.75rem, 2.2vw, 2.5rem); | |||
| 1045 | - | } | |||
| 1046 | - | ||||
| 1047 | - | .hero-heading { | |||
| 1048 | - | display: flex; | |||
| 1049 | - | flex-direction: column; | |||
| 1050 | - | gap: 0.5rem; | |||
| 1051 | - | max-width: 36rem; | |||
| 1052 | - | } | |||
| 1053 | - | ||||
| 1054 | - | .hero-title { | |||
| 1055 | - | font-size: clamp(2rem, 3vw, 2.75rem); | |||
| 1056 | - | font-weight: 700; | |||
| 1057 | - | letter-spacing: -0.01em; | |||
| 1058 | - | margin: 0; | |||
| 1059 | - | } | |||
| 1060 | - | ||||
| 1061 | - | .hero-subtitle { | |||
| 1062 | - | font-size: 1rem; | |||
| 1063 | - | line-height: 1.6; | |||
| 1064 | - | color: var(--dynamic-muted); | |||
| 1065 | - | margin: 0; | |||
| 1066 | - | } | |||
| 1067 | - | ||||
| 1068 | - | .quick-access-wrapper { | |||
| 1069 | - | display: flex; | |||
| 1070 | - | flex-direction: column; | |||
| 1071 | - | gap: 1rem; | |||
| 1072 | - | } | |||
| 1073 | - | ||||
| 1074 | - | .quick-access-grid { | |||
| 1075 | - | display: flex; | |||
| 1076 | - | flex-wrap: wrap; | |||
| 1077 | - | gap: 1rem; | |||
| 1078 | - | } | |||
| 1079 | - | ||||
| 1080 | - | .quick-access-item { | |||
| 1081 | - | flex: 1 1 min(16rem, 100%); | |||
| 1082 | - | min-width: 12rem; | |||
| 1083 | - | display: flex; | |||
| 1084 | - | align-items: center; | |||
| 1085 | - | gap: 1rem; | |||
| 1086 | - | border: 2px solid var(--dynamic-border); | |||
| 1087 | - | background-color: var(--dynamic-surface-variant); | |||
| 1088 | - | padding: 0.9rem 1.1rem; | |||
| 1089 | - | cursor: pointer; | |||
| 1090 | - | border-radius: 0; | |||
| 1091 | - | transition: | |||
| 1092 | - | border-color 200ms ease, | |||
| 1093 | - | background-color 200ms ease; | |||
| 1094 | - | } | |||
| 1095 | - | ||||
| 1096 | - | .quick-access-item:hover, | |||
| 1097 | - | .quick-access-item:focus-visible { | |||
| 1098 | - | border-color: var(--dynamic-accent); | |||
| 1099 | - | outline: none; | |||
| 1100 | - | } | |||
| 1101 | - | ||||
| 1102 | - | .quick-access-favicon { | |||
| 1103 | - | width: 48px; | |||
| 1104 | - | height: 48px; | |||
| 1105 | - | border: 2px solid var(--dynamic-border); | |||
| 1106 | - | background-color: var(--dynamic-surface); | |||
| 1107 | - | display: flex; | |||
| 1108 | - | align-items: center; | |||
| 1109 | - | justify-content: center; | |||
| 1110 | - | flex-shrink: 0; | |||
| 1111 | - | border-radius: 0; | |||
| 1112 | - | } | |||
| 1113 | - | ||||
| 1114 | - | .quick-access-favicon-img { | |||
| 1115 | - | width: 70%; | |||
| 1116 | - | height: 70%; | |||
| 1117 | - | object-fit: contain; | |||
| 1118 | - | } | |||
| 1119 | - | ||||
| 1120 | - | .quick-access-fallback { | |||
| 1121 | - | width: 100%; | |||
| 1122 | - | height: 100%; | |||
| 1123 | - | display: flex; | |||
| 1124 | - | align-items: center; | |||
| 1125 | - | justify-content: center; | |||
| 1126 | - | color: var(--dynamic-muted); | |||
| 1127 | - | } | |||
| 1128 | - | ||||
| 1129 | - | .quick-access-meta { | |||
| 1130 | - | flex: 1; | |||
| 1131 | - | min-width: 0; | |||
| 1132 | - | display: flex; | |||
| 1133 | - | flex-direction: column; | |||
| 1134 | - | gap: 0.125rem; | |||
| 1135 | - | } | |||
| 1136 | - | ||||
| 1137 | - | .quick-access-title { | |||
| 1138 | - | font-size: 1rem; | |||
| 1139 | - | font-weight: 600; | |||
| 1140 | - | color: var(--dynamic-text); | |||
| 1141 | - | white-space: nowrap; | |||
| 1142 | - | overflow: hidden; | |||
| 1143 | - | text-overflow: ellipsis; | |||
| 1144 | - | } | |||
| 1145 | 1070 | ||||
| 1146 | - | .quick-access-domain { | |||
| 1147 | - | font-size: 0.85rem; | |||
| 1148 | - | color: var(--dynamic-muted); | |||
| 1149 | - | white-space: nowrap; | |||
| 1150 | - | overflow: hidden; | |||
| 1151 | - | text-overflow: ellipsis; | |||
| 1152 | - | } | |||
| 1153 | - | ||||
| 1154 | - | .quick-access-visits { | |||
| 1155 | - | font-size: 0.82rem; | |||
| 1156 | - | color: var(--dynamic-accent); | |||
| 1157 | - | text-transform: uppercase; | |||
| 1158 | - | letter-spacing: 0.05em; | |||
| 1159 | - | font-weight: 600; | |||
| 1160 | - | flex-shrink: 0; | |||
| 1161 | - | } | |||
| 1162 | 1071 | ||||
| 1163 | .loading { | 1072 | .loading { | ||
| 1164 | padding: 1.25rem; | 1073 | padding: 1.25rem; | ||
| @@ -1211,7 +1120,7 @@ | |||||
| 1211 | min-height: 0; | 1120 | min-height: 0; | ||
| 1212 | } | 1121 | } | ||
| 1213 | 1122 | ||||
| 1214 | - | .insights-panel { | 1123 | + | .quick-access-panel { |
| 1215 | flex: 1 1 35%; | 1124 | flex: 1 1 35%; | ||
| 1216 | min-width: min(24rem, 100%); | 1125 | min-width: min(24rem, 100%); | ||
| 1217 | display: flex; | 1126 | display: flex; | ||
| @@ -1264,28 +1173,31 @@ | |||||
| 1264 | flex: 1; | 1173 | flex: 1; | ||
| 1265 | min-height: 0; | 1174 | min-height: 0; | ||
| 1266 | overflow-y: auto; | 1175 | overflow-y: auto; | ||
| 1267 | - | padding-right: 0.5rem; | 1176 | + | background-color: var(--dynamic-surface-variant); |
| 1177 | + | border: 2px solid var(--dynamic-border); | |||
| 1178 | + | padding: 0.5rem; | |||
| 1268 | } | 1179 | } | ||
| 1269 | 1180 | ||||
| 1270 | .history-item { | 1181 | .history-item { | ||
| 1271 | display: flex; | 1182 | display: flex; | ||
| 1272 | align-items: flex-start; | 1183 | align-items: flex-start; | ||
| 1273 | - | gap: 1rem; | 1184 | + | gap: 0.75rem; |
| 1274 | - | border: 2px solid var(--dynamic-border); | 1185 | + | border-bottom: 1px solid var(--dynamic-border); |
| 1275 | background-color: var(--dynamic-surface-variant); | 1186 | background-color: var(--dynamic-surface-variant); | ||
| 1276 | - | padding: 1rem 1.1rem; | 1187 | + | padding: 0.75rem 0.5rem; |
| 1277 | cursor: pointer; | 1188 | cursor: pointer; | ||
| 1278 | - | min-height: 80px; | |||
| 1279 | - | border-radius: 0; | |||
| 1280 | transition: | 1189 | transition: | ||
| 1281 | - | border-color 200ms ease, | 1190 | + | background-color 200ms ease, |
| 1282 | - | background-color 200ms ease; | 1191 | + | border-color 200ms ease; |
| 1192 | + | } | |||
| 1193 | + | ||||
| 1194 | + | .history-item:last-child { | |||
| 1195 | + | border-bottom: none; | |||
| 1283 | } | 1196 | } | ||
| 1284 | 1197 | ||||
| 1285 | .history-item:hover, | 1198 | .history-item:hover, | ||
| 1286 | .history-item:focus-visible { | 1199 | .history-item:focus-visible { | ||
| 1287 | - | border-color: var(--dynamic-accent); | 1200 | + | background-color: var(--dynamic-surface); |
| 1288 | - | background-color: var(--dynamic-bg); | |||
| 1289 | outline: none; | 1201 | outline: none; | ||
| 1290 | } | 1202 | } | ||
| 1291 | 1203 | ||||
| @@ -1327,11 +1239,12 @@ | |||||
| 1327 | min-width: 0; | 1239 | min-width: 0; | ||
| 1328 | display: flex; | 1240 | display: flex; | ||
| 1329 | flex-direction: column; | 1241 | flex-direction: column; | ||
| 1330 | - | gap: 0.125rem; | 1242 | + | gap: 0.25rem; |
| 1331 | justify-content: center; | 1243 | justify-content: center; | ||
| 1244 | + | overflow: hidden; | |||
| 1332 | } | 1245 | } | ||
| 1333 | 1246 | ||||
| 1334 | - | .history-item-top { | 1247 | + | .history-item-main { |
| 1335 | display: flex; | 1248 | display: flex; | ||
| 1336 | align-items: flex-start; | 1249 | align-items: flex-start; | ||
| 1337 | justify-content: space-between; | 1250 | justify-content: space-between; | ||
| @@ -1343,31 +1256,19 @@ | |||||
| 1343 | color: var(--dynamic-text); | 1256 | color: var(--dynamic-text); | ||
| 1344 | white-space: nowrap; | 1257 | white-space: nowrap; | ||
| 1345 | overflow: hidden; | 1258 | overflow: hidden; | ||
| 1346 | - | text-overflow: ellipsis; | 1259 | + | flex: 1; |
| 1260 | + | position: relative; | |||
| 1261 | + | mask-image: linear-gradient(to right, black 70%, transparent 100%); | |||
| 1262 | + | -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%); | |||
| 1347 | } | 1263 | } | ||
| 1348 | 1264 | ||||
| 1265 | + | ||||
| 1349 | .history-time { | 1266 | .history-time { | ||
| 1350 | font-size: 0.85rem; | 1267 | font-size: 0.85rem; | ||
| 1351 | color: var(--dynamic-muted); | 1268 | color: var(--dynamic-muted); | ||
| 1352 | flex-shrink: 0; | 1269 | flex-shrink: 0; | ||
| 1353 | } | 1270 | } | ||
| 1354 | 1271 | ||||
| 1355 | - | .history-item-bottom { | |||
| 1356 | - | display: flex; | |||
| 1357 | - | align-items: baseline; | |||
| 1358 | - | gap: 0.5rem; | |||
| 1359 | - | overflow: hidden; | |||
| 1360 | - | position: relative; | |||
| 1361 | - | } | |||
| 1362 | - | ||||
| 1363 | - | .history-domain { | |||
| 1364 | - | font-size: 0.9rem; | |||
| 1365 | - | font-weight: 500; | |||
| 1366 | - | color: var(--dynamic-text); | |||
| 1367 | - | white-space: nowrap; | |||
| 1368 | - | flex-shrink: 0; | |||
| 1369 | - | } | |||
| 1370 | - | ||||
| 1371 | .history-url { | 1272 | .history-url { | ||
| 1372 | font-size: 0.8rem; | 1273 | font-size: 0.8rem; | ||
| 1373 | color: var(--dynamic-muted); | 1274 | color: var(--dynamic-muted); | ||
| @@ -1379,11 +1280,41 @@ | |||||
| 1379 | monospace; | 1280 | monospace; | ||
| 1380 | overflow: hidden; | 1281 | overflow: hidden; | ||
| 1381 | flex: 1; | 1282 | flex: 1; | ||
| 1283 | + | min-width: 0; | |||
| 1382 | position: relative; | 1284 | position: relative; | ||
| 1383 | mask-image: linear-gradient(to right, black 70%, transparent 100%); | 1285 | mask-image: linear-gradient(to right, black 70%, transparent 100%); | ||
| 1384 | -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%); | 1286 | -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%); | ||
| 1385 | } | 1287 | } | ||
| 1386 | 1288 | ||||
| 1289 | + | .history-url :global(.history-url-domain) { | |||
| 1290 | + | color: var(--dynamic-text) !important; | |||
| 1291 | + | font-weight: 600; | |||
| 1292 | + | } | |||
| 1293 | + | ||||
| 1294 | + | .history-actions { | |||
| 1295 | + | display: flex; | |||
| 1296 | + | flex-shrink: 0; | |||
| 1297 | + | } | |||
| 1298 | + | ||||
| 1299 | + | .history-pin { | |||
| 1300 | + | display: flex; | |||
| 1301 | + | align-items: center; | |||
| 1302 | + | justify-content: center; | |||
| 1303 | + | width: 38px; | |||
| 1304 | + | border-left: 2px solid var(--dynamic-border); | |||
| 1305 | + | background-color: var(--dynamic-surface); | |||
| 1306 | + | color: var(--dynamic-muted); | |||
| 1307 | + | transition: background-color 200ms ease, color 200ms ease; | |||
| 1308 | + | cursor: pointer; | |||
| 1309 | + | } | |||
| 1310 | + | ||||
| 1311 | + | .history-pin:hover, | |||
| 1312 | + | .history-pin:focus-visible { | |||
| 1313 | + | color: var(--dynamic-accent); | |||
| 1314 | + | background-color: var(--dynamic-bg); | |||
| 1315 | + | outline: none; | |||
| 1316 | + | } | |||
| 1317 | + | ||||
| 1387 | .history-delete { | 1318 | .history-delete { | ||
| 1388 | display: flex; | 1319 | display: flex; | ||
| 1389 | align-items: center; | 1320 | align-items: center; | ||
| @@ -1416,66 +1347,68 @@ | |||||
| 1416 | font-size: 0.85rem; | 1347 | font-size: 0.85rem; | ||
| 1417 | } | 1348 | } | ||
| 1418 | 1349 | ||||
| 1419 | - | .insights-body { | 1350 | + | .quick-access-body { |
| 1420 | - | gap: 1.5rem; | 1351 | + | gap: 1rem; |
| 1421 | - | } | |||
| 1422 | - | ||||
| 1423 | - | .insight-block { | |||
| 1424 | - | display: flex; | |||
| 1425 | - | flex-direction: column; | |||
| 1426 | - | gap: 0.9rem; | |||
| 1427 | - | } | |||
| 1428 | - | ||||
| 1429 | - | .block-title { | |||
| 1430 | - | font-size: 0.85rem; | |||
| 1431 | - | text-transform: uppercase; | |||
| 1432 | - | letter-spacing: 0.08em; | |||
| 1433 | - | color: var(--dynamic-muted); | |||
| 1434 | - | margin: 0; | |||
| 1435 | } | 1352 | } | ||
| 1436 | 1353 | ||||
| 1437 | - | .insights-list { | 1354 | + | .quick-access-grid-compact { |
| 1438 | display: flex; | 1355 | display: flex; | ||
| 1439 | flex-direction: column; | 1356 | flex-direction: column; | ||
| 1440 | - | gap: 0.75rem; | 1357 | + | gap: 0.5rem; |
| 1358 | + | background-color: var(--dynamic-surface-variant); | |||
| 1359 | + | border: 2px solid var(--dynamic-border); | |||
| 1360 | + | padding: 0.5rem; | |||
| 1441 | } | 1361 | } | ||
| 1442 | 1362 | ||||
| 1443 | - | .insight-item { | 1363 | + | .quick-access-item-compact { |
| 1444 | display: flex; | 1364 | display: flex; | ||
| 1445 | align-items: center; | 1365 | align-items: center; | ||
| 1446 | gap: 0.75rem; | 1366 | gap: 0.75rem; | ||
| 1447 | - | border: 2px solid var(--dynamic-border); | 1367 | + | border-bottom: 1px solid var(--dynamic-border); |
| 1448 | background-color: var(--dynamic-surface-variant); | 1368 | background-color: var(--dynamic-surface-variant); | ||
| 1449 | - | padding: 0.85rem 1rem; | 1369 | + | padding: 0.5rem 0.25rem; |
| 1450 | cursor: pointer; | 1370 | cursor: pointer; | ||
| 1451 | - | border-radius: 0; | 1371 | + | transition: background-color 200ms ease; |
| 1452 | - | transition: | |||
| 1453 | - | border-color 200ms ease, | |||
| 1454 | - | background-color 200ms ease; | |||
| 1455 | } | 1372 | } | ||
| 1456 | 1373 | ||||
| 1457 | - | .insight-item:hover, | 1374 | + | .quick-access-item-compact:last-child { |
| 1458 | - | .insight-item:focus-visible { | 1375 | + | border-bottom: none; |
| 1459 | - | border-color: var(--dynamic-accent); | 1376 | + | } |
| 1377 | + | ||||
| 1378 | + | .quick-access-item-compact:hover, | |||
| 1379 | + | .quick-access-item-compact:focus-visible { | |||
| 1380 | + | background-color: var(--dynamic-surface); | |||
| 1460 | outline: none; | 1381 | outline: none; | ||
| 1461 | } | 1382 | } | ||
| 1462 | 1383 | ||||
| 1463 | - | .insight-rank { | 1384 | + | .quick-access-favicon-compact { |
| 1464 | - | width: 2rem; | 1385 | + | width: 32px; |
| 1465 | - | height: 2rem; | 1386 | + | height: 32px; |
| 1466 | border: 2px solid var(--dynamic-border); | 1387 | border: 2px solid var(--dynamic-border); | ||
| 1467 | - | background-color: var(--dynamic-surface); | 1388 | + | background-color: var(--dynamic-surface-variant); |
| 1468 | display: flex; | 1389 | display: flex; | ||
| 1469 | align-items: center; | 1390 | align-items: center; | ||
| 1470 | justify-content: center; | 1391 | justify-content: center; | ||
| 1471 | - | font-weight: 600; | |||
| 1472 | - | font-size: 0.9rem; | |||
| 1473 | - | color: var(--dynamic-accent); | |||
| 1474 | flex-shrink: 0; | 1392 | flex-shrink: 0; | ||
| 1475 | border-radius: 0; | 1393 | border-radius: 0; | ||
| 1476 | } | 1394 | } | ||
| 1477 | 1395 | ||||
| 1478 | - | .insight-body { | 1396 | + | .quick-access-favicon-img-compact { |
| 1397 | + | width: 70%; | |||
| 1398 | + | height: 70%; | |||
| 1399 | + | object-fit: contain; | |||
| 1400 | + | } | |||
| 1401 | + | ||||
| 1402 | + | .quick-access-fallback-compact { | |||
| 1403 | + | width: 100%; | |||
| 1404 | + | height: 100%; | |||
| 1405 | + | display: flex; | |||
| 1406 | + | align-items: center; | |||
| 1407 | + | justify-content: center; | |||
| 1408 | + | color: var(--dynamic-muted); | |||
| 1409 | + | } | |||
| 1410 | + | ||||
| 1411 | + | .quick-access-meta-compact { | |||
| 1479 | flex: 1; | 1412 | flex: 1; | ||
| 1480 | min-width: 0; | 1413 | min-width: 0; | ||
| 1481 | display: flex; | 1414 | display: flex; | ||
| @@ -1483,48 +1416,63 @@ | |||||
| 1483 | gap: 0.125rem; | 1416 | gap: 0.125rem; | ||
| 1484 | } | 1417 | } | ||
| 1485 | 1418 | ||||
| 1486 | - | .insight-title { | 1419 | + | .quick-access-title-compact { |
| 1420 | + | font-size: 0.95rem; | |||
| 1487 | font-weight: 600; | 1421 | font-weight: 600; | ||
| 1488 | color: var(--dynamic-text); | 1422 | color: var(--dynamic-text); | ||
| 1489 | white-space: nowrap; | 1423 | white-space: nowrap; | ||
| 1490 | overflow: hidden; | 1424 | overflow: hidden; | ||
| 1491 | - | text-overflow: ellipsis; | 1425 | + | position: relative; |
| 1426 | + | mask-image: linear-gradient(to right, black 70%, transparent 100%); | |||
| 1427 | + | -webkit-mask-image: linear-gradient(to right, black 70%, transparent 100%); | |||
| 1492 | } | 1428 | } | ||
| 1493 | 1429 | ||||
| 1494 | - | .insight-domain { | 1430 | + | .quick-access-domain-compact { |
| 1495 | - | font-size: 0.85rem; | 1431 | + | font-size: 0.8rem; |
| 1496 | color: var(--dynamic-muted); | 1432 | color: var(--dynamic-muted); | ||
| 1497 | white-space: nowrap; | 1433 | white-space: nowrap; | ||
| 1498 | overflow: hidden; | 1434 | overflow: hidden; | ||
| 1499 | text-overflow: ellipsis; | 1435 | text-overflow: ellipsis; | ||
| 1500 | } | 1436 | } | ||
| 1501 | 1437 | ||||
| 1502 | - | .insight-visits { | 1438 | + | .quick-access-actions-compact { |
| 1503 | - | font-size: 0.78rem; | 1439 | + | display: flex; |
| 1504 | - | color: var(--dynamic-accent); | 1440 | + | align-items: center; |
| 1505 | - | text-transform: uppercase; | 1441 | + | gap: 0.5rem; |
| 1506 | - | letter-spacing: 0.05em; | |||
| 1507 | - | font-weight: 600; | |||
| 1508 | flex-shrink: 0; | 1442 | flex-shrink: 0; | ||
| 1509 | } | 1443 | } | ||
| 1510 | 1444 | ||||
| 1511 | - | .stats-grid { | 1445 | + | .quick-access-pin-compact { |
| 1512 | display: flex; | 1446 | display: flex; | ||
| 1513 | - | flex-wrap: wrap; | 1447 | + | align-items: center; |
| 1514 | - | gap: 0.85rem; | 1448 | + | justify-content: center; |
| 1515 | - | } | 1449 | + | width: 24px; |
| 1516 | - | 1450 | + | height: 24px; | |
| 1517 | - | .stat-item { | 1451 | + | border: 1px solid var(--dynamic-border); |
| 1518 | - | flex: 1 1 160px; | |||
| 1519 | - | border: 2px solid var(--dynamic-border); | |||
| 1520 | background-color: var(--dynamic-surface-variant); | 1452 | background-color: var(--dynamic-surface-variant); | ||
| 1521 | - | padding: 1rem; | 1453 | + | color: var(--dynamic-muted); |
| 1522 | - | display: flex; | 1454 | + | transition: background-color 200ms ease, color 200ms ease; |
| 1523 | - | flex-direction: column; | 1455 | + | cursor: pointer; |
| 1524 | - | gap: 0.35rem; | |||
| 1525 | border-radius: 0; | 1456 | border-radius: 0; | ||
| 1526 | } | 1457 | } | ||
| 1527 | 1458 | ||||
| 1459 | + | .quick-access-pin-compact:hover, | |||
| 1460 | + | .quick-access-pin-compact:focus-visible { | |||
| 1461 | + | color: var(--dynamic-accent); | |||
| 1462 | + | background-color: var(--dynamic-surface); | |||
| 1463 | + | outline: none; | |||
| 1464 | + | } | |||
| 1465 | + | ||||
| 1466 | + | .quick-access-visits-compact { | |||
| 1467 | + | font-size: 0.75rem; | |||
| 1468 | + | color: var(--dynamic-accent); | |||
| 1469 | + | text-transform: uppercase; | |||
| 1470 | + | letter-spacing: 0.05em; | |||
| 1471 | + | font-weight: 600; | |||
| 1472 | + | flex-shrink: 0; | |||
| 1473 | + | } | |||
| 1474 | + | ||||
| 1475 | + | ||||
| 1528 | .stat-value { | 1476 | .stat-value { | ||
| 1529 | font-size: 1.4rem; | 1477 | font-size: 1.4rem; | ||
| 1530 | font-weight: 600; | 1478 | font-weight: 600; | ||
| @@ -1538,6 +1486,26 @@ | |||||
| 1538 | letter-spacing: 0.08em; | 1486 | letter-spacing: 0.08em; | ||
| 1539 | } | 1487 | } | ||
| 1540 | 1488 | ||||
| 1489 | + | .stats-grid-horizontal { | |||
| 1490 | + | display: flex; | |||
| 1491 | + | flex-wrap: wrap; | |||
| 1492 | + | gap: 1rem; | |||
| 1493 | + | justify-content: space-between; | |||
| 1494 | + | } | |||
| 1495 | + | ||||
| 1496 | + | .stat-item-horizontal { | |||
| 1497 | + | flex: 1; | |||
| 1498 | + | min-width: 120px; | |||
| 1499 | + | border: 2px solid var(--dynamic-border); | |||
| 1500 | + | background-color: var(--dynamic-surface-variant); | |||
| 1501 | + | padding: 0.75rem; | |||
| 1502 | + | display: flex; | |||
| 1503 | + | flex-direction: column; | |||
| 1504 | + | gap: 0.25rem; | |||
| 1505 | + | text-align: center; | |||
| 1506 | + | border-radius: 0; | |||
| 1507 | + | } | |||
| 1508 | + | ||||
| 1541 | .shortcuts-panel { | 1509 | .shortcuts-panel { | ||
| 1542 | gap: 0; | 1510 | gap: 0; | ||
| 1543 | } | 1511 | } | ||
| @@ -1587,7 +1555,7 @@ | |||||
| 1587 | /* Responsive Design */ | 1555 | /* Responsive Design */ | ||
| 1588 | @media (max-width: 1200px) { | 1556 | @media (max-width: 1200px) { | ||
| 1589 | .history-panel, | 1557 | .history-panel, | ||
| 1590 | - | .insights-panel { | 1558 | + | .quick-access-panel { |
| 1591 | min-width: min(28rem, 100%); | 1559 | min-width: min(28rem, 100%); | ||
| 1592 | } | 1560 | } | ||
| 1593 | } | 1561 | } | ||
| @@ -1602,13 +1570,12 @@ | |||||
| 1602 | } | 1570 | } | ||
| 1603 | 1571 | ||||
| 1604 | .history-panel, | 1572 | .history-panel, | ||
| 1605 | - | .insights-panel { | 1573 | + | .quick-access-panel { |
| 1606 | min-width: 100%; | 1574 | min-width: 100%; | ||
| 1575 | + | max-width: 100%; | |||
| 1576 | + | width: 100%; | |||
| 1607 | } | 1577 | } | ||
| 1608 | 1578 | ||||
| 1609 | - | .quick-access-item { | |||
| 1610 | - | flex: 1 1 calc(50% - 1rem); | |||
| 1611 | - | } | |||
| 1612 | } | 1579 | } | ||
| 1613 | 1580 | ||||
| 1614 | @media (max-width: 640px) { | 1581 | @media (max-width: 640px) { | ||
| @@ -1618,29 +1585,33 @@ | |||||
| 1618 | } | 1585 | } | ||
| 1619 | 1586 | ||||
| 1620 | .panel-body, | 1587 | .panel-body, | ||
| 1621 | - | .panel-header, | 1588 | + | .panel-header { |
| 1622 | - | .hero-panel { | |||
| 1623 | padding-left: 1.25rem; | 1589 | padding-left: 1.25rem; | ||
| 1624 | padding-right: 1.25rem; | 1590 | padding-right: 1.25rem; | ||
| 1625 | } | 1591 | } | ||
| 1626 | 1592 | ||||
| 1627 | - | .quick-access-item { | |||
| 1628 | - | flex: 1 1 100%; | |||
| 1629 | - | } | |||
| 1630 | 1593 | ||||
| 1631 | .history-item { | 1594 | .history-item { | ||
| 1632 | flex-direction: column; | 1595 | flex-direction: column; | ||
| 1633 | align-items: flex-start; | 1596 | align-items: flex-start; | ||
| 1634 | } | 1597 | } | ||
| 1635 | 1598 | ||||
| 1636 | - | .history-delete { | 1599 | + | .history-actions { |
| 1637 | width: 100%; | 1600 | width: 100%; | ||
| 1638 | - | border-left: 0; | |||
| 1639 | border-top: 2px solid var(--dynamic-border); | 1601 | border-top: 2px solid var(--dynamic-border); | ||
| 1640 | - | align-self: stretch; | |||
| 1641 | } | 1602 | } | ||
| 1642 | 1603 | ||||
| 1643 | - | .history-item-top { | 1604 | + | .history-pin, |
| 1605 | + | .history-delete { | |||
| 1606 | + | flex: 1; | |||
| 1607 | + | border-left: 0; | |||
| 1608 | + | } | |||
| 1609 | + | ||||
| 1610 | + | .history-pin { | |||
| 1611 | + | border-right: 1px solid var(--dynamic-border); | |||
| 1612 | + | } | |||
| 1613 | + | ||||
| 1614 | + | .history-item-main { | |||
| 1644 | flex-direction: column; | 1615 | flex-direction: column; | ||
| 1645 | align-items: flex-start; | 1616 | align-items: flex-start; | ||
| 1646 | gap: 0.35rem; | 1617 | gap: 0.35rem; | ||
| @@ -1649,5 +1620,37 @@ | |||||
| 1649 | .history-time { | 1620 | .history-time { | ||
| 1650 | order: 3; | 1621 | order: 3; | ||
| 1651 | } | 1622 | } | ||
| 1623 | + | ||||
| 1624 | + | .stats-grid-horizontal { | |||
| 1625 | + | flex-direction: column; | |||
| 1626 | + | gap: 0.75rem; | |||
| 1627 | + | } | |||
| 1628 | + | ||||
| 1629 | + | .stat-item-horizontal { | |||
| 1630 | + | min-width: 100%; | |||
| 1631 | + | } | |||
| 1632 | + | } | |||
| 1633 | + | ||||
| 1634 | + | .show-more-button { | |||
| 1635 | + | width: 100%; | |||
| 1636 | + | padding: 0.75rem 1rem; | |||
| 1637 | + | border: 2px solid var(--dynamic-border); | |||
| 1638 | + | background-color: var(--dynamic-surface-variant); | |||
| 1639 | + | color: var(--dynamic-text); | |||
| 1640 | + | font-size: 0.9rem; | |||
| 1641 | + | font-weight: 500; | |||
| 1642 | + | cursor: pointer; | |||
| 1643 | + | transition: | |||
| 1644 | + | background-color 200ms ease, | |||
| 1645 | + | border-color 200ms ease; | |||
| 1646 | + | border-radius: 0; | |||
| 1647 | + | margin-top: 0.5rem; | |||
| 1648 | + | } | |||
| 1649 | + | ||||
| 1650 | + | .show-more-button:hover, | |||
| 1651 | + | .show-more-button:focus-visible { | |||
| 1652 | + | background-color: var(--dynamic-surface); | |||
| 1653 | + | border-color: var(--dynamic-accent); | |||
| 1654 | + | outline: none; | |||
| 1652 | } | 1655 | } | ||
| 1653 | </style> | 1656 | </style> | ||