Remove link preview tooltip

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 21:42:11 -04:00
parent ba8173572e
commit b77a9bb027
-346
View File
@@ -958,351 +958,5 @@
</script>
<!-- Link Preview Tooltip -->
<div id="link-preview-tooltip" class="link-preview-tooltip">
<div id="preview-content"></div>
</div>
<style>
.link-preview-tooltip {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 10000;
width: 400px;
max-width: calc(100vw - 4rem);
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 1.25rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
pointer-events: auto;
cursor: pointer;
font-family: et-book, Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;
opacity: 0;
transform: translateY(100%);
transition: opacity 0.08s ease, transform 0.08s ease;
}
.link-preview-tooltip:hover {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
}
.link-preview-tooltip.visible {
opacity: 1;
transform: translateY(0);
}
a.link-preview-active {
background: rgba(243, 156, 18, 0.15);
box-shadow: 0 0 8px rgba(243, 156, 18, 0.4);
border-radius: 3px;
padding: 0.1rem 0.2rem;
margin: -0.1rem -0.2rem;
}
.preview-header {
display: flex;
align-items: flex-start;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.preview-icon {
width: 36px;
height: 36px;
flex-shrink: 0;
border-radius: 4px;
}
.preview-title {
font-size: 1.05rem;
font-weight: 600;
color: #222;
line-height: 1.4;
margin-bottom: 0.25rem;
}
.preview-meta {
font-size: 0.8rem;
color: #888;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.preview-meta-item {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.preview-excerpt {
font-size: 0.875rem;
color: #555;
line-height: 1.6;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
body.dark-mode .link-preview-tooltip {
background: rgba(26, 26, 26, 0.98);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
body.dark-mode .preview-title {
color: #e5e5e5;
}
body.dark-mode .preview-meta {
color: #999;
}
body.dark-mode .preview-excerpt {
color: #bbb;
border-top-color: rgba(255, 255, 255, 0.1);
}
@media (prefers-color-scheme: dark) {
.link-preview-tooltip {
background: rgba(26, 26, 26, 0.98);
border-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
.preview-title {
color: #e5e5e5;
}
.preview-meta {
color: #999;
}
.preview-excerpt {
color: #bbb;
border-top-color: rgba(255, 255, 255, 0.1);
}
}
@media (max-width: 768px) {
.link-preview-tooltip {
bottom: 1rem;
right: 1rem;
width: calc(100vw - 2rem);
padding: 1rem;
}
}
</style>
<script>
(function() {
const tooltip = document.getElementById('link-preview-tooltip');
const previewContent = document.getElementById('preview-content');
let currentLink = null;
let hideTimeout = null;
const cache = {};
// Find all links in article content (internal and external)
document.addEventListener('DOMContentLoaded', function() {
// Disable tooltips on index/archive pages
const path = window.location.pathname;
if (path.match(/\/(sidenotes|outlines|connections|quotes|terms|archive)/)) {
return; // Don't attach tooltip listeners on these pages
}
const articleLinks = document.querySelectorAll('article a[href]');
articleLinks.forEach(link => {
const href = link.getAttribute('href');
// Skip links in nav, breadcrumbs, etc.
if (link.closest('nav') || link.closest('.breadcrumbs')) return;
// Skip links in special index pages (sidenotes, outlines, etc.)
if (link.closest('.sidenotes-list') || link.closest('.outline-list') || link.closest('.sidenote-entry')) return;
// Skip sidenote links (links with # anchors to same page)
if (link.classList.contains('sidenote-link') || link.classList.contains('outline-link')) return;
// Skip anchor links and mailto/tel links
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return;
// Skip any link that contains a hash (likely anchor link)
if (href.includes('#')) return;
// Skip alternate format links (.md, .pdf)
if (href.endsWith('.md') || href.endsWith('.pdf')) return;
// Support internal links and external http/https links
if (href.startsWith('/') || href.startsWith('http://') || href.startsWith('https://')) {
link.addEventListener('mouseenter', handleLinkHover);
link.addEventListener('mouseleave', handleLinkLeave);
}
});
});
function handleLinkHover(e) {
const link = e.currentTarget;
const href = link.getAttribute('href');
clearTimeout(hideTimeout);
currentLink = link;
// Show tooltip after slight delay
setTimeout(() => {
if (currentLink === link) {
showPreview(link, href);
}
}, 500);
}
function handleLinkLeave() {
currentLink = null;
tooltip.classList.remove('visible');
hideTimeout = setTimeout(() => {
tooltip.style.display = 'none';
}, 200);
}
async function showPreview(link, href) {
// Check cache first
if (cache[href]) {
displayPreview(link, cache[href]);
return;
}
// Handle external links differently (can't fetch due to CORS)
const isExternal = href.startsWith('http://') || href.startsWith('https://');
if (isExternal) {
try {
const url = new URL(href);
const data = {
title: link.textContent || url.hostname,
excerpt: `External link: ${url.hostname}`,
url: href,
isExternal: true
};
cache[href] = data;
displayPreview(link, data);
} catch (e) {
console.error('Invalid URL:', e);
}
return;
}
try {
// Fetch internal page
const response = await fetch(href);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract metadata
const title = doc.querySelector('h1')?.textContent ||
doc.querySelector('title')?.textContent ||
'Untitled';
// Try to find reading time badge
const readingTimeBadge = doc.querySelector('.reading-time-badge');
const readingTime = readingTimeBadge?.textContent || null;
// Try to find post date
const dateElement = doc.querySelector('.post-subtitle');
const dateMatch = dateElement?.textContent.match(/[A-Z][a-z]+ \d{1,2}, \d{4}/);
const date = dateMatch ? dateMatch[0] : null;
// Get first meaningful paragraph
const paragraphs = doc.querySelectorAll('article section p:not(.subtitle):not(.sidenote):not(.marginnote)');
let excerpt = '';
for (const p of paragraphs) {
const text = p.textContent.trim();
if (text.length > 50) {
excerpt = text;
break;
}
}
if (excerpt.length > 200) {
excerpt = excerpt.substring(0, 200).trim() + '...';
}
// Try to get icon
const icon = doc.querySelector('.post-title-icon')?.src ||
doc.querySelector('.archive-post-icon')?.src ||
doc.querySelector('.article-icon')?.src ||
null;
// Calculate word count
const articleText = doc.querySelector('article section')?.textContent || '';
const wordCount = Math.round(articleText.trim().split(/\s+/).filter(w => w.length > 0).length);
const data = {
title,
readingTime,
date,
excerpt,
icon,
wordCount
};
cache[href] = data;
displayPreview(link, data);
} catch (error) {
console.error('Failed to fetch preview:', error);
}
}
function displayPreview(link, data) {
let html = '<div class="preview-header">';
if (data.icon) {
html += `<img src="${data.icon}" class="preview-icon" alt="">`;
}
html += `<div>`;
html += `<div class="preview-title">${escapeHtml(data.title || 'Untitled')}</div>`;
const metaParts = [];
if (data.readingTime) metaParts.push(data.readingTime);
else if (data.wordCount && data.wordCount > 0) {
const estReadingTime = Math.ceil(data.wordCount / 200);
metaParts.push(`~${estReadingTime} min read`);
}
if (data.wordCount && data.wordCount > 0) metaParts.push(`${data.wordCount} words`);
if (data.date) metaParts.push(data.date);
if (metaParts.length > 0) {
html += `<div class="preview-meta">${metaParts.join(' • ')}</div>`;
}
html += `</div></div>`;
if (data.excerpt && data.excerpt.length > 0) {
html += `<div class="preview-excerpt">${escapeHtml(data.excerpt)}</div>`;
}
previewContent.innerHTML = html;
// Show tooltip in fixed position
tooltip.style.display = 'block';
// Trigger animation
setTimeout(() => {
tooltip.classList.add('visible');
}, 10);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})();
</script>
</body>
</html>