mirror of
https://github.com/kennethreitz/kennethreitz.org.git
synced 2026-06-05 22:50:17 +00:00
Add reading progress indicator and enhance search with result highlighting
- Add reading progress bar for longer essays with smooth animation - Implement search result snippets with highlighted query terms - Tighten directory listing styles for better compactness - Add back-to-parent navigation links in directory views 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2360,11 +2360,37 @@ def api_search():
|
||||
item_display_path = f"{display_path}/{item.name}" if display_path else item.name
|
||||
|
||||
if query in node_name or query in node_path or query in node_content:
|
||||
# Generate snippet with highlighted search terms for markdown files
|
||||
snippet = ""
|
||||
if item.suffix == '.md' and node_content and query in node_content:
|
||||
# Find the first occurrence of the query in content
|
||||
query_pos = node_content.find(query)
|
||||
if query_pos != -1:
|
||||
# Extract context around the query (200 chars before and after)
|
||||
start = max(0, query_pos - 100)
|
||||
end = min(len(node_content), query_pos + len(query) + 100)
|
||||
snippet_text = node_content[start:end]
|
||||
|
||||
# Clean up the snippet (remove markdown syntax)
|
||||
import re
|
||||
snippet_text = re.sub(r'[#*`_\[\]()]', '', snippet_text)
|
||||
snippet_text = re.sub(r'\s+', ' ', snippet_text).strip()
|
||||
|
||||
# Highlight the search term (case-insensitive)
|
||||
snippet = re.sub(f'({re.escape(query)})', r'<mark>\1</mark>', snippet_text, flags=re.IGNORECASE)
|
||||
|
||||
# Add ellipsis if snippet is truncated
|
||||
if start > 0:
|
||||
snippet = "..." + snippet
|
||||
if end < len(node_content):
|
||||
snippet = snippet + "..."
|
||||
|
||||
result = {
|
||||
'name': item.name,
|
||||
'type': 'directory' if item.is_dir() else ('article' if item.suffix == '.md' else 'file'),
|
||||
'path': relative_path,
|
||||
'display_path': item_display_path,
|
||||
'snippet': snippet,
|
||||
'relevance': 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,18 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Reading progress indicator */
|
||||
.reading-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0%;
|
||||
height: 2px;
|
||||
background: #333;
|
||||
z-index: 1000;
|
||||
transition: width 0.1s ease-out;
|
||||
}
|
||||
|
||||
/* Dropdown navigation styles */
|
||||
.nav-dropdown {
|
||||
position: relative;
|
||||
@@ -174,6 +186,9 @@
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Reading progress bar -->
|
||||
<div class="reading-progress" id="reading-progress"></div>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<nav>
|
||||
@@ -461,6 +476,42 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Reading progress indicator
|
||||
function updateReadingProgress() {
|
||||
const progressBar = document.getElementById('reading-progress');
|
||||
if (!progressBar) return;
|
||||
|
||||
const mainContent = document.querySelector('main');
|
||||
if (!mainContent) return;
|
||||
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight;
|
||||
const winHeight = window.innerHeight;
|
||||
const scrollPercent = scrollTop / (docHeight - winHeight);
|
||||
const scrollPercentRounded = Math.round(scrollPercent * 100);
|
||||
|
||||
progressBar.style.width = scrollPercentRounded + '%';
|
||||
|
||||
// Only show progress bar if there's meaningful content to scroll
|
||||
if (docHeight > winHeight * 1.5) {
|
||||
progressBar.style.opacity = '1';
|
||||
} else {
|
||||
progressBar.style.opacity = '0';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize reading progress on pages with substantial content
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check if this is a content page (essay/article) rather than an index
|
||||
const isContentPage = document.querySelector('main').textContent.length > 2000;
|
||||
|
||||
if (isContentPage) {
|
||||
window.addEventListener('scroll', updateReadingProgress);
|
||||
window.addEventListener('resize', updateReadingProgress);
|
||||
updateReadingProgress(); // Initial call
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -82,6 +82,20 @@
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.search-result-snippet {
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.search-result-snippet mark {
|
||||
background: #fff3cd;
|
||||
padding: 0.1rem 0.2rem;
|
||||
border-radius: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
@@ -211,6 +225,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const iconHtml = result.unique_icon ?
|
||||
`<img src="${result.unique_icon}" alt="Icon for ${result.name}" class="search-result-icon">` : '';
|
||||
|
||||
const snippetHtml = result.snippet ?
|
||||
`<div class="search-result-snippet">${result.snippet}</div>` : '';
|
||||
|
||||
resultElement.innerHTML = `
|
||||
<div class="search-result-header">
|
||||
${iconHtml}
|
||||
@@ -220,6 +237,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<span class="search-result-type">${typeIcon} ${result.type}</span>
|
||||
<span class="search-result-path">${result.display_path}</span>
|
||||
</div>
|
||||
${snippetHtml}
|
||||
`;
|
||||
|
||||
resultsContainer.appendChild(resultElement);
|
||||
|
||||
Reference in New Issue
Block a user