Browse Source

highlight current section and pad header

master
silberengel 8 months ago
parent
commit
97d54e64ac
  1. 27
      src/app.css
  2. 105
      src/lib/components/publications/TableOfContents.svelte
  3. 2
      src/lib/consts.ts
  4. 3
      src/lib/data_structures/publication_tree.ts
  5. 6
      src/lib/ndk.ts
  6. 14
      src/lib/utils/relay_management.ts

27
src/app.css

@ -288,6 +288,8 @@
/* Rendered publication content */ /* Rendered publication content */
.publication-leather { .publication-leather {
@apply flex flex-col space-y-4; @apply flex flex-col space-y-4;
scroll-margin-top: 150px;
scroll-behavior: smooth;
h1, h1,
h2, h2,
@ -441,6 +443,21 @@
scrollbar-color: rgba(156, 163, 175, 0.5) transparent !important; 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 { .description-textarea {
min-height: 100% !important; 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 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; @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;
}
} }

105
src/lib/components/publications/TableOfContents.svelte

@ -10,6 +10,7 @@
SidebarItem, SidebarItem,
} from "flowbite-svelte"; } from "flowbite-svelte";
import Self from "./TableOfContents.svelte"; import Self from "./TableOfContents.svelte";
import { onMount, onDestroy } from "svelte";
let { depth, onSectionFocused } = $props<{ let { depth, onSectionFocused } = $props<{
rootAddress: string; rootAddress: string;
@ -32,6 +33,10 @@
return newEntries; return newEntries;
}); });
// Track the currently visible section
let currentVisibleSection = $state<string | null>(null);
let observer: IntersectionObserver;
function setEntryExpanded(address: string, expanded: boolean = false) { function setEntryExpanded(address: string, expanded: boolean = false) {
const entry = toc.getEntry(address); const entry = toc.getEntry(address);
if (!entry) { if (!entry) {
@ -41,6 +46,100 @@
toc.expandedMap.set(address, expanded); toc.expandedMap.set(address, expanded);
entry.resolveChildren(); 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();
}
});
</script> </script>
<!-- TODO: Figure out how to style indentations. --> <!-- TODO: Figure out how to style indentations. -->
@ -50,18 +149,20 @@
{@const address = entry.address} {@const address = entry.address}
{@const expanded = toc.expandedMap.get(address) ?? false} {@const expanded = toc.expandedMap.get(address) ?? false}
{@const isLeaf = toc.leaves.has(address)} {@const isLeaf = toc.leaves.has(address)}
{@const isVisible = isEntryVisible(address)}
{#if isLeaf} {#if isLeaf}
<SidebarItem <SidebarItem
label={entry.title} label={entry.title}
href={`#${address}`} href={`#${address}`}
spanClass="px-2 text-ellipsis" spanClass="px-2 text-ellipsis"
onclick={() => onSectionFocused?.(address)} class={isVisible ? "toc-highlight" : ""}
onclick={() => handleSectionClick(address)}
/> />
{:else} {:else}
{@const childDepth = depth + 1} {@const childDepth = depth + 1}
<SidebarDropdownWrapper <SidebarDropdownWrapper
label={entry.title} label={entry.title}
btnClass="flex items-center p-2 w-full font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-primary-50 dark:text-white dark:hover:bg-primary-800" btnClass="flex items-center p-2 w-full font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-primary-50 dark:text-white dark:hover:bg-primary-800 {isVisible ? 'toc-highlight' : ''}"
bind:isOpen={() => expanded, (open) => setEntryExpanded(address, open)} bind:isOpen={() => expanded, (open) => setEntryExpanded(address, open)}
> >
<Self rootAddress={address} depth={childDepth} {onSectionFocused} /> <Self rootAddress={address} depth={childDepth} {onSectionFocused} />

2
src/lib/consts.ts

@ -1,3 +1,5 @@
// AI SHOULD NEVER CHANGE THIS FILE
export const wikiKind = 30818; export const wikiKind = 30818;
export const indexKind = 30040; export const indexKind = 30040;
export const zettelKinds = [30041, 30818]; export const zettelKinds = [30041, 30818];

3
src/lib/data_structures/publication_tree.ts

@ -558,9 +558,10 @@ export class PublicationTree implements AsyncIterable<NDKEvent | null> {
currentEvent = this.#events.get(currentAddress!); currentEvent = this.#events.get(currentAddress!);
if (!currentEvent) { if (!currentEvent) {
throw new Error( console.warn(
`[PublicationTree] Event with address ${currentAddress} not found.`, `[PublicationTree] Event with address ${currentAddress} not found.`,
); );
return null;
} }
// Stop immediately if the target of the search is found. // Stop immediately if the target of the search is found.

6
src/lib/ndk.ts

@ -349,7 +349,7 @@ function createRelayWithAuth(url: string, ndk: NDK): NDKRelay {
const connectionTimeout = setTimeout(() => { const connectionTimeout = setTimeout(() => {
console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`); console.warn(`[NDK.ts] Connection timeout for ${secureUrl}`);
relay.disconnect(); relay.disconnect();
}, 10000); // 10 second timeout }, 5000); // 5 second timeout
// Set up custom authentication handling only if user is signed in // Set up custom authentication handling only if user is signed in
if (ndk.signer && ndk.activeUser) { if (ndk.signer && ndk.activeUser) {
@ -509,7 +509,7 @@ export function initNdk(): NDK {
// Connect with better error handling and reduced retry attempts // Connect with better error handling and reduced retry attempts
let retryCount = 0; let retryCount = 0;
const maxRetries = 2; const maxRetries = 1; // Reduce to 1 retry
const attemptConnection = async () => { const attemptConnection = async () => {
try { try {
@ -526,7 +526,7 @@ export function initNdk(): NDK {
if (retryCount < maxRetries) { if (retryCount < maxRetries) {
retryCount++; retryCount++;
console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`); console.debug(`[NDK.ts] Attempting to reconnect (${retryCount}/${maxRetries})...`);
setTimeout(attemptConnection, 3000); setTimeout(attemptConnection, 2000); // Reduce timeout to 2 seconds
} else { } else {
console.warn("[NDK.ts] Max retries reached, continuing with limited functionality"); console.warn("[NDK.ts] Max retries reached, continuing with limited functionality");
// Still try to update relay stores even if connection failed // Still try to update relay stores even if connection failed

14
src/lib/utils/relay_management.ts

@ -325,7 +325,7 @@ export async function getUserOutboxRelays(ndk: NDK, user: NDKUser): Promise<stri
*/ */
async function testRelaySet(relayUrls: string[], ndk: NDK): Promise<string[]> { async function testRelaySet(relayUrls: string[], ndk: NDK): Promise<string[]> {
const workingRelays: string[] = []; 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) { for (let i = 0; i < relayUrls.length; i += maxConcurrent) {
const batch = relayUrls.slice(i, i + maxConcurrent); const batch = relayUrls.slice(i, i + maxConcurrent);
@ -335,12 +335,16 @@ async function testRelaySet(relayUrls: string[], ndk: NDK): Promise<string[]> {
const result = await testRelayConnection(url, ndk); const result = await testRelayConnection(url, ndk);
return result.connected ? url : null; return result.connected ? url : null;
} catch (error) { } catch (error) {
console.debug(`[relay_management.ts] Failed to test relay ${url}:`, error);
return null; return null;
} }
}); });
const batchResults = await Promise.all(batchPromises); const batchResults = await Promise.allSettled(batchPromises);
const batchWorkingRelays = batchResults.filter((url): url is string => url !== null); const batchWorkingRelays = batchResults
.filter((result): result is PromiseFulfilledResult<string | null> => result.status === 'fulfilled')
.map(result => result.value)
.filter((url): url is string => url !== null);
workingRelays.push(...batchWorkingRelays); workingRelays.push(...batchWorkingRelays);
} }
@ -369,13 +373,13 @@ export async function buildCompleteRelaySet(
try { try {
userOutboxRelays = await getUserOutboxRelays(ndk, user); userOutboxRelays = await getUserOutboxRelays(ndk, user);
} catch (error) { } catch (error) {
// Silently ignore user relay fetch errors console.debug('[relay_management.ts] Error fetching user outbox relays:', error);
} }
try { try {
userLocalRelays = await getUserLocalRelays(ndk, user); userLocalRelays = await getUserLocalRelays(ndk, user);
} catch (error) { } catch (error) {
// Silently ignore user local relay fetch errors console.debug('[relay_management.ts] Error fetching user local relays:', error);
} }
try { try {

Loading…
Cancel
Save