From 97d54e64ac51945db5d0216eb7e2a7663cc0c4fa Mon Sep 17 00:00:00 2001 From: silberengel Date: Fri, 18 Jul 2025 20:51:30 +0200 Subject: [PATCH] highlight current section and pad header --- src/app.css | 27 +++++ .../publications/TableOfContents.svelte | 105 +++++++++++++++++- src/lib/consts.ts | 2 + src/lib/data_structures/publication_tree.ts | 3 +- src/lib/ndk.ts | 6 +- src/lib/utils/relay_management.ts | 14 ++- 6 files changed, 146 insertions(+), 11 deletions(-) diff --git a/src/app.css b/src/app.css index fbaca62..da20412 100644 --- a/src/app.css +++ b/src/app.css @@ -288,6 +288,8 @@ /* Rendered publication content */ .publication-leather { @apply flex flex-col space-y-4; + scroll-margin-top: 150px; + scroll-behavior: smooth; h1, h2, @@ -441,6 +443,21 @@ scrollbar-color: rgba(156, 163, 175, 0.5) transparent !important; } + /* Section scroll behavior */ + section[id] { + scroll-margin-top: 150px; + } + + /* Ensure section headers maintain their padding */ + section[id] h1, + section[id] h2, + section[id] h3, + section[id] h4, + section[id] h5, + section[id] h6 { + @apply pt-4; + } + .description-textarea { min-height: 100% !important; } @@ -486,4 +503,14 @@ @apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border-s-4 border-primary-200 rounded shadow-none px-4 py-2; @apply focus:border-primary-600 dark:focus:border-primary-400; } + + /* Table of Contents highlighting */ + .toc-highlight { + @apply bg-primary-100 dark:bg-primary-800 border-l-4 border-primary-600 dark:border-primary-400; + transition: all 0.2s ease-in-out; + } + + .toc-highlight:hover { + @apply bg-primary-200 dark:bg-primary-700; + } } diff --git a/src/lib/components/publications/TableOfContents.svelte b/src/lib/components/publications/TableOfContents.svelte index e50bce0..0a16108 100644 --- a/src/lib/components/publications/TableOfContents.svelte +++ b/src/lib/components/publications/TableOfContents.svelte @@ -10,6 +10,7 @@ SidebarItem, } from "flowbite-svelte"; import Self from "./TableOfContents.svelte"; + import { onMount, onDestroy } from "svelte"; let { depth, onSectionFocused } = $props<{ rootAddress: string; @@ -32,6 +33,10 @@ return newEntries; }); + // Track the currently visible section + let currentVisibleSection = $state(null); + let observer: IntersectionObserver; + function setEntryExpanded(address: string, expanded: boolean = false) { const entry = toc.getEntry(address); if (!entry) { @@ -41,6 +46,100 @@ toc.expandedMap.set(address, expanded); entry.resolveChildren(); } + + function handleSectionClick(address: string) { + // Smooth scroll to the section + const element = document.getElementById(address); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + + onSectionFocused?.(address); + } + + // Check if an entry is currently visible + function isEntryVisible(address: string): boolean { + return currentVisibleSection === address; + } + + // Set up intersection observer to track visible sections + onMount(() => { + observer = new IntersectionObserver( + (entries) => { + // Find the section that is most visible in the viewport + let maxIntersectionRatio = 0; + let mostVisibleSection: string | null = null; + + entries.forEach((entry) => { + if (entry.isIntersecting && entry.intersectionRatio > maxIntersectionRatio) { + maxIntersectionRatio = entry.intersectionRatio; + mostVisibleSection = entry.target.id; + } + }); + + if (mostVisibleSection && mostVisibleSection !== currentVisibleSection) { + currentVisibleSection = mostVisibleSection; + } + }, + { + threshold: [0, 0.25, 0.5, 0.75, 1], + rootMargin: "-20% 0px -20% 0px", // Consider section visible when it's in the middle 60% of the viewport + } + ); + + // Function to observe all section elements + function observeSections() { + const sections = document.querySelectorAll('section[id]'); + sections.forEach((section) => { + observer.observe(section); + }); + } + + // Initial observation + observeSections(); + + // Set up a mutation observer to watch for new sections being added + const mutationObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + // Check if the added node is a section with an id + if (element.tagName === 'SECTION' && element.id) { + observer.observe(element); + } + // Check if the added node contains sections + const sections = element.querySelectorAll?.('section[id]'); + if (sections) { + sections.forEach((section) => { + observer.observe(section); + }); + } + } + }); + }); + }); + + // Start observing the document body for changes + mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }); + + return () => { + observer.disconnect(); + mutationObserver.disconnect(); + }; + }); + + onDestroy(() => { + if (observer) { + observer.disconnect(); + } + }); @@ -50,18 +149,20 @@ {@const address = entry.address} {@const expanded = toc.expandedMap.get(address) ?? false} {@const isLeaf = toc.leaves.has(address)} + {@const isVisible = isEntryVisible(address)} {#if isLeaf} onSectionFocused?.(address)} + class={isVisible ? "toc-highlight" : ""} + onclick={() => handleSectionClick(address)} /> {:else} {@const childDepth = depth + 1} expanded, (open) => setEntryExpanded(address, open)} > diff --git a/src/lib/consts.ts b/src/lib/consts.ts index b6df380..998cbec 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,3 +1,5 @@ +// AI SHOULD NEVER CHANGE THIS FILE + export const wikiKind = 30818; export const indexKind = 30040; export const zettelKinds = [30041, 30818]; diff --git a/src/lib/data_structures/publication_tree.ts b/src/lib/data_structures/publication_tree.ts index dbb20cb..2fefad7 100644 --- a/src/lib/data_structures/publication_tree.ts +++ b/src/lib/data_structures/publication_tree.ts @@ -558,9 +558,10 @@ export class PublicationTree implements AsyncIterable { currentEvent = this.#events.get(currentAddress!); if (!currentEvent) { - throw new Error( + console.warn( `[PublicationTree] Event with address ${currentAddress} not found.`, ); + return null; } // Stop immediately if the target of the search is found. diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index cd13df2..6e1120e 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -349,7 +349,7 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { const connectionTimeout = setTimeout(() => { console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); relay.disconnect(); - }, 10000); // 10 second timeout + }, 5000); // 5 second timeout // Set up custom authentication handling only if user is signed in if (ndk.signer && ndk.activeUser) { @@ -509,7 +509,7 @@ export function initNdk(): NDK { // Connect with better error handling and reduced retry attempts let retryCount = 0; - const maxRetries = 2; + const maxRetries = 1; // Reduce to 1 retry const attemptConnection = async () => { try { @@ -526,7 +526,7 @@ export function initNdk(): NDK { if (retryCount < maxRetries) { retryCount++; console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`); - setTimeout(attemptConnection, 3000); + setTimeout(attemptConnection, 2000); // Reduce timeout to 2 seconds } else { console.warn("[NDK.ts] Max retries reached, continuing with limited functionality"); // Still try to update relay stores even if connection failed diff --git a/src/lib/utils/relay_management.ts b/src/lib/utils/relay_management.ts index e02c7f8..09aa5ac 100644 --- a/src/lib/utils/relay_management.ts +++ b/src/lib/utils/relay_management.ts @@ -325,7 +325,7 @@ export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise { const workingRelays: string[] = []; - const maxConcurrent = 3; // Test 3 relays at a time to avoid overwhelming them + const maxConcurrent = 2; // Reduce to 2 relays at a time to avoid overwhelming them for (let i = 0; i < relayUrls.length; i += maxConcurrent) { const batch = relayUrls.slice(i, i + maxConcurrent); @@ -335,12 +335,16 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise { const result = await testRelayConnection(url, ndk); return result.connected ? url : null; } catch (error) { + console.debug(`[relay_management.ts] Failed to test relay ${url}:`, error); return null; } }); - const batchResults = await Promise.all(batchPromises); - const batchWorkingRelays = batchResults.filter((url): url is string => url !== null); + const batchResults = await Promise.allSettled(batchPromises); + const batchWorkingRelays = batchResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map(result => result.value) + .filter((url): url is string => url !== null); workingRelays.push(...batchWorkingRelays); } @@ -369,13 +373,13 @@ export async function buildCompleteRelaySet( try { userOutboxRelays = await getUserOutboxRelays(ndk, user); } catch (error) { - // Silently ignore user relay fetch errors + console.debug('[relay_management.ts] Error fetching user outbox relays:', error); } try { userLocalRelays = await getUserLocalRelays(ndk, user); } catch (error) { - // Silently ignore user local relay fetch errors + console.debug('[relay_management.ts] Error fetching user local relays:', error); } try {