diff --git a/shaarli-pro/css/style.css b/shaarli-pro/css/style.css index a428f5a..cdf74ed 100644 --- a/shaarli-pro/css/style.css +++ b/shaarli-pro/css/style.css @@ -623,20 +623,21 @@ input:checked+.theme-slider:before { border-color: var(--primary); } -/* ===== Search Overlay ===== */ +/* ===== Search Overlay (Spotlight Style) ===== */ .search-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; - background: var(--overlay-bg); + background: rgba(0, 0, 0, 0.55); z-index: 500; display: none; align-items: flex-start; justify-content: center; - padding-top: 15vh; + padding-top: 12vh; animation: fadeIn 0.2s ease; + backdrop-filter: blur(4px); } .search-overlay.show { @@ -655,94 +656,332 @@ input:checked+.theme-slider:before { .search-modal { width: 90%; - max-width: 600px; - background: var(--bg-card); - border-radius: 1rem; - box-shadow: var(--shadow-xl); + max-width: 580px; + background: #ffffff; + border-radius: 12px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.05); overflow: hidden; - animation: slideUp 0.2s ease; + animation: slideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1); +} + +[data-theme="dark"] .search-modal { + background: var(--bg-card); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1); } @keyframes slideUp { from { opacity: 0; - transform: translateY(20px); + transform: translateY(20px) scale(0.98); } to { opacity: 1; - transform: translateY(0); + transform: translateY(0) scale(1); } } +/* Search Header (Input Zone) */ .search-modal-header { display: flex; align-items: center; padding: 1rem 1.25rem; - border-bottom: 1px solid var(--border); gap: 0.75rem; } -.search-modal-icon { - color: var(--text-muted); - font-size: 1.25rem; -} - .search-modal-input { flex: 1; border: none; background: transparent; - font-size: 1.125rem; - color: var(--text-main); + font-size: 1rem; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + color: #111827; outline: none; + caret-color: var(--primary); +} + +[data-theme="dark"] .search-modal-input { + color: var(--text-main); } .search-modal-input::placeholder { + color: #9ca3af; +} + +[data-theme="dark"] .search-modal-input::placeholder { color: var(--text-muted); } +/* Pill Buttons */ .search-modal-actions { display: flex; gap: 0.5rem; } -.search-type-btn { - padding: 0.375rem 0.75rem; - border-radius: 0.375rem; - border: 1px solid var(--border); - background: var(--bg-body); - color: var(--text-secondary); - font-size: 0.8rem; - cursor: pointer; +.search-pill-btn { display: flex; align-items: center; - gap: 0.25rem; + gap: 0.375rem; + padding: 0.5rem 0.875rem; + border-radius: 9999px; + border: none; + font-size: 0.8125rem; + font-weight: 500; + font-family: inherit; + cursor: pointer; transition: all 0.15s ease; } -.search-type-btn:hover, -.search-type-btn.active { - background: var(--primary); - border-color: var(--primary); - color: white; +.search-pill-btn i { + font-size: 0.9rem; } -.search-hint { - padding: 0.75rem 1.25rem; +/* Tags pill - Gray style */ +.search-pill-tags { + background: #E5E7EB; + color: #374151; +} + +.search-pill-tags:hover { + background: #D1D5DB; +} + +.search-pill-tags.active { + background: #3B82F6; + color: #ffffff; +} + +[data-theme="dark"] .search-pill-tags { + background: #374151; + color: #D1D5DB; +} + +[data-theme="dark"] .search-pill-tags:hover { + background: #4B5563; +} + +[data-theme="dark"] .search-pill-tags.active { + background: #3B82F6; + color: #ffffff; +} + +/* Search pill - Blue style */ +.search-pill-search { + background: #E5E7EB; + color: #374151; +} + +.search-pill-search:hover { + background: #D1D5DB; +} + +.search-pill-search.active { + background: #3B82F6; + color: #ffffff; +} + +[data-theme="dark"] .search-pill-search { + background: #374151; + color: #D1D5DB; +} + +[data-theme="dark"] .search-pill-search:hover { + background: #4B5563; +} + +[data-theme="dark"] .search-pill-search.active { + background: #3B82F6; + color: #ffffff; +} + +/* Separator */ +.search-separator { + height: 1px; + background: #E5E7EB; + margin: 0; +} + +[data-theme="dark"] .search-separator { + background: var(--border); +} + +/* Results List */ +.search-results { + max-height: 320px; + overflow-y: auto; + padding: 0.5rem 0; +} + +.search-results-empty { + padding: 1.5rem 1.25rem; + text-align: center; +} + +.search-results-hint { + color: #9ca3af; + font-size: 0.875rem; +} + +[data-theme="dark"] .search-results-hint { color: var(--text-muted); - font-size: 0.8rem; +} + +/* Result Item */ +.search-result-item { + display: block; + padding: 0.625rem 1.25rem; + color: #374151; + font-size: 0.9375rem; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + cursor: pointer; + transition: background 0.1s ease; + text-decoration: none; + line-height: 1.5; +} + +.search-result-item:hover, +.search-result-item.selected { + background: #F3F4F6; + color: #111827; +} + +[data-theme="dark"] .search-result-item { + color: var(--text-secondary); +} + +[data-theme="dark"] .search-result-item:hover, +[data-theme="dark"] .search-result-item.selected { + background: var(--bg-card-hover); + color: var(--text-main); +} + +/* Highlight (Fuzzy Matching) */ +.search-result-item mark, +.search-highlight { + background-color: #FFFF00; + color: #000000; + padding: 0 2px; + border-radius: 2px; +} + +[data-theme="dark"] .search-result-item mark, +[data-theme="dark"] .search-highlight { + background-color: #FDE047; + color: #000000; +} + +/* No Results */ +.search-no-results { + padding: 1.5rem 1.25rem; + text-align: center; + color: #9ca3af; + font-size: 0.875rem; +} + +[data-theme="dark"] .search-no-results { + color: var(--text-muted); +} + +/* Footer */ +.search-footer { + padding: 0.625rem 1.25rem; + background: #F9FAFB; + border-top: 1px solid #E5E7EB; +} + +[data-theme="dark"] .search-footer { background: var(--bg-body); + border-top-color: var(--border); +} + +.search-footer-hint { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + color: #9ca3af; + font-size: 0.75rem; +} + +[data-theme="dark"] .search-footer-hint { + color: var(--text-muted); } .search-kbd { - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.5rem; padding: 0.125rem 0.375rem; + background: #ffffff; + border: 1px solid #D1D5DB; + border-radius: 4px; + font-family: 'Inter', system-ui, sans-serif; + font-size: 0.6875rem; + font-weight: 500; + color: #6B7280; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] .search-kbd { background: var(--bg-card); - border: 1px solid var(--border); - border-radius: 0.25rem; - font-family: monospace; - font-size: 0.75rem; - margin-left: 0.25rem; + border-color: var(--border); + color: var(--text-muted); +} + +/* Loading state */ +.search-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + color: #9ca3af; +} + +.search-loading::after { + content: ''; + width: 20px; + height: 20px; + border: 2px solid #E5E7EB; + border-top-color: #3B82F6; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Result item with icon */ +.search-result-item-content { + display: flex; + align-items: center; + gap: 0.625rem; +} + +.search-result-icon { + flex-shrink: 0; + width: 1.25rem; + height: 1.25rem; + display: flex; + align-items: center; + justify-content: center; + color: #9ca3af; + font-size: 0.875rem; +} + +[data-theme="dark"] .search-result-icon { + color: var(--text-muted); +} + +.search-result-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* ===== Content Container ===== */ diff --git a/shaarli-pro/js/script.js b/shaarli-pro/js/script.js index 5588571..08df9ba 100644 --- a/shaarli-pro/js/script.js +++ b/shaarli-pro/js/script.js @@ -44,18 +44,250 @@ document.addEventListener('DOMContentLoaded', () => { mobileMenuBtn?.addEventListener('click', toggleSidebar); sidebarOverlay?.addEventListener('click', toggleSidebar); - // ===== Search Overlay ===== + // ===== Search Overlay (Spotlight Style) ===== const searchOverlay = document.getElementById('search-overlay'); const searchToggleBtn = document.getElementById('search-toggle-btn'); const searchModalInput = document.getElementById('search-modal-input'); + const searchResults = document.getElementById('search-results'); + const searchTagsBtn = document.getElementById('search-tags-btn'); + const searchAllBtn = document.getElementById('search-all-btn'); + const searchForm = document.getElementById('search-form'); + + let searchMode = 'search'; // 'search' or 'tags' + let selectedIndex = -1; + let searchTimeout = null; + let cachedBookmarks = null; + let cachedTags = null; + + // Highlight matching text with tags + function highlightMatch(text, query) { + if (!query || query.length === 0) return text; + + // Escape special regex characters in query + const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(${escapedQuery})`, 'gi'); + return text.replace(regex, '$1'); + } + + // Fuzzy search - matches substring anywhere in text + function fuzzyMatch(text, query) { + if (!query || query.length === 0) return true; + return text.toLowerCase().includes(query.toLowerCase()); + } + + // Fetch all unique tags from the page + function fetchTags() { + if (cachedTags) return cachedTags; + try { + const tagElements = document.querySelectorAll('.link-tag, .tag-link, [class*="tag"]'); + const tagsSet = new Set(); + + tagElements.forEach(el => { + const tagText = el.textContent.trim(); + if (tagText && tagText.length > 0 && !tagText.includes('•')) { + tagsSet.add(tagText); + } + }); + + // Convert to array and sort alphabetically + cachedTags = Array.from(tagsSet).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + return cachedTags; + } catch (e) { + console.error('Failed to fetch tags:', e); + return []; + } + } + + // Fetch bookmarks for live search + async function fetchBookmarks() { + if (cachedBookmarks) return cachedBookmarks; + + try { + // Try to get bookmarks from page (if already loaded) + const linkElements = document.querySelectorAll('.link-outer'); + if (linkElements.length > 0) { + cachedBookmarks = Array.from(linkElements).map(el => ({ + id: el.dataset.id, + title: el.querySelector('.link-title a')?.textContent || el.querySelector('.link-title')?.textContent || '', + url: el.querySelector('.link-url a')?.href || el.querySelector('.link-title a')?.href || '', + tags: Array.from(el.querySelectorAll('.link-tag')).map(t => t.textContent.trim()), + description: el.querySelector('.link-description')?.textContent || '' + })); + return cachedBookmarks; + } + } catch (e) { + console.error('Failed to fetch bookmarks:', e); + } + + return []; + } + + // Render search results (tags or bookmarks) + function renderResults(results, query, isTagMode = false) { + if (!searchResults) return; + + if (results.length === 0) { + if (query && query.length > 0) { + searchResults.innerHTML = ` +
+ No results found for "${escapeHtml(query)}" +
+ `; + } else { + searchResults.innerHTML = ` +
+ Start typing to see tag suggestions... +
+ `; + } + return; + } + + let html; + if (isTagMode) { + // Render tags + html = results.slice(0, 15).map((tag, index) => { + const highlightedTag = highlightMatch(escapeHtml(tag), query); + return ` +
+
+ + ${highlightedTag} +
+
+ `; + }).join(''); + } else { + // Render bookmarks + html = results.slice(0, 10).map((item, index) => { + const highlightedTitle = highlightMatch(escapeHtml(item.title), query); + return ` +
+
+ + ${highlightedTitle} +
+
+ `; + }).join(''); + } + + searchResults.innerHTML = html; + + // Add click handlers to results + searchResults.querySelectorAll('.search-result-item').forEach(item => { + item.addEventListener('click', () => { + if (isTagMode) { + const tag = item.dataset.tag; + if (tag) { + // Navigate to tag search + window.location.href = shaarli.basePath + '/?searchtags=' + encodeURIComponent(tag); + } + } else { + const url = item.dataset.url; + if (url) { + window.location.href = url; + } + } + }); + }); + } + + // Escape HTML to prevent XSS + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + // Perform live search + async function performSearch(query) { + const tags = fetchTags(); + + if (!query || query.length === 0) { + // Show all tags when empty + renderResults(tags.slice(0, 15), '', true); + return; + } + + // Filter tags with fuzzy matching + const results = tags.filter(tag => fuzzyMatch(tag, query)); + renderResults(results, query, true); + } + + // Update selected result + function updateSelection(newIndex) { + const items = searchResults?.querySelectorAll('.search-result-item'); + if (!items || items.length === 0) return; + + // Clamp index + if (newIndex < 0) newIndex = items.length - 1; + if (newIndex >= items.length) newIndex = 0; + + selectedIndex = newIndex; + + items.forEach((item, index) => { + item.classList.toggle('selected', index === selectedIndex); + }); + + // Scroll into view + items[selectedIndex]?.scrollIntoView({ block: 'nearest' }); + } + + // Navigate to selected result + function navigateToSelected() { + const selected = searchResults?.querySelector('.search-result-item.selected'); + if (selected) { + // Check if it's a tag + const tag = selected.dataset.tag; + if (tag) { + window.location.href = shaarli.basePath + '/?searchtags=' + encodeURIComponent(tag); + return true; + } + // Otherwise check for URL + const url = selected.dataset.url; + if (url) { + window.location.href = url; + return true; + } + } + return false; + } function openSearch() { searchOverlay?.classList.add('show'); - setTimeout(() => searchModalInput?.focus(), 100); + selectedIndex = -1; + setTimeout(() => { + searchModalInput?.focus(); + // Trigger initial search if there's existing text + if (searchModalInput?.value) { + performSearch(searchModalInput.value); + } + }, 100); } function closeSearch() { searchOverlay?.classList.remove('show'); + selectedIndex = -1; + } + + // Toggle search mode (tags vs search) + function setSearchMode(mode) { + searchMode = mode; + + searchTagsBtn?.classList.toggle('active', mode === 'tags'); + searchAllBtn?.classList.toggle('active', mode === 'search'); + + if (searchModalInput) { + searchModalInput.name = mode === 'tags' ? 'searchtags' : 'searchterm'; + // Re-run search with new mode + performSearch(searchModalInput.value); + } } searchToggleBtn?.addEventListener('click', openSearch); @@ -67,24 +299,74 @@ document.addEventListener('DOMContentLoaded', () => { } }); - // Keyboard shortcut: S to open search, ESC to close + // Search mode toggle buttons + searchTagsBtn?.addEventListener('click', (e) => { + e.preventDefault(); + setSearchMode('tags'); + }); + + searchAllBtn?.addEventListener('click', (e) => { + // Only prevent default if not submitting + if (selectedIndex >= 0) { + e.preventDefault(); + navigateToSelected(); + } + }); + + // Live search on input + searchModalInput?.addEventListener('input', (e) => { + const query = e.target.value; + + // Debounce search + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + selectedIndex = -1; + performSearch(query); + }, 150); + }); + + // Keyboard shortcuts document.addEventListener('keydown', (e) => { - // Don't trigger if typing in an input - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { - if (e.key === 'Escape') { + const isSearchOpen = searchOverlay?.classList.contains('show'); + const isTyping = e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA'; + + // Handle ESC - always close search/filter + if (e.key === 'Escape') { + if (isSearchOpen) { closeSearch(); + e.preventDefault(); + } + closeFilterPanel(); + return; + } + + // If search is open, handle navigation + if (isSearchOpen && e.target === searchModalInput) { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + updateSelection(selectedIndex + 1); + break; + case 'ArrowUp': + e.preventDefault(); + updateSelection(selectedIndex - 1); + break; + case 'Enter': + // If an item is selected, navigate to it + if (selectedIndex >= 0 && navigateToSelected()) { + e.preventDefault(); + } + // Otherwise, submit the form normally + break; } return; } - if (e.key === 's' || e.key === 'S') { + // S to open search (when not typing) + if (!isTyping && (e.key === 's' || e.key === 'S')) { e.preventDefault(); openSearch(); } - if (e.key === 'Escape') { - closeSearch(); - closeFilterPanel(); - } }); // ===== Filter Panel ===== @@ -360,26 +642,6 @@ document.addEventListener('DOMContentLoaded', () => { form.submit(); } - // ===== Search Type Toggle ===== - const searchTagsBtn = document.getElementById('search-tags-btn'); - const searchAllBtn = document.getElementById('search-all-btn'); - const searchForm = document.getElementById('search-form'); - - searchTagsBtn?.addEventListener('click', (e) => { - e.preventDefault(); - searchModalInput.name = 'searchtags'; - searchAllBtn?.classList.remove('active'); - searchTagsBtn?.classList.add('active'); - searchForm?.submit(); - }); - - searchAllBtn?.addEventListener('click', (e) => { - e.preventDefault(); - searchModalInput.name = 'searchterm'; - searchTagsBtn?.classList.remove('active'); - searchAllBtn?.classList.add('active'); - searchForm?.submit(); - }); // ===== Thumbnail Update ===== const thumbnailsPage = document.querySelector('.page-thumbnails'); diff --git a/shaarli-pro/page.header.html b/shaarli-pro/page.header.html index 4deda0d..d986414 100644 --- a/shaarli-pro/page.header.html +++ b/shaarli-pro/page.header.html @@ -207,27 +207,38 @@ - +
-
- -
-
- Press ESC to close or Enter to search +
+
+
+ Start typing to see suggestions... +
+
+