Browse Source

Merges pull request #81

Issue#312 major qc revamp -- Part 1

I'm merging this because it at least looks neater and the infinite scroll makes it more usable.
master
silberengel 3 months ago
parent
commit
5ad97c11f6
No known key found for this signature in database
GPG Key ID: 962BEC8725790894
  1. 188
      src/app.css
  2. 8
      src/app.html
  3. 12
      src/lib/a/nav/ANavbar.svelte
  4. 3
      src/lib/a/primitives/ADetails.svelte
  5. 11
      src/lib/a/primitives/ANostrUser.svelte
  6. 48
      src/lib/components/Navigation.svelte
  7. 137
      src/lib/components/cards/BlogHeader.svelte
  8. 14
      src/lib/components/publications/CommentButton.svelte
  9. 109
      src/lib/components/publications/CommentLayer.svelte
  10. 255
      src/lib/components/publications/HighlightLayer.svelte
  11. 26
      src/lib/components/publications/HighlightSelectionHandler.svelte
  12. 1298
      src/lib/components/publications/Publication.svelte
  13. 6
      src/lib/components/publications/PublicationHeader.svelte
  14. 90
      src/lib/components/publications/PublicationSection.svelte
  15. 135
      src/lib/components/publications/TableOfContents.svelte
  16. 62
      src/lib/components/util/ArticleNav.svelte
  17. 18
      src/lib/components/util/CardActions.svelte
  18. 14
      src/lib/components/util/Details.svelte
  19. 36
      src/lib/data_structures/websocket_pool.ts
  20. 9
      src/lib/services/publisher.ts
  21. 2
      src/lib/snippets/PublicationSnippets.svelte
  22. 13
      src/lib/stores/themeStore.ts
  23. 11
      src/routes/+layout.svelte
  24. 2
      src/routes/publication/[type]/[identifier]/+page.svelte
  25. 28
      src/styles/a/cards.css
  26. 2
      src/styles/base.css
  27. 12
      src/styles/notifications.css
  28. 2
      src/styles/publications.css
  29. 20
      src/styles/visualize.css
  30. 11
      src/theme-tokens.css

188
src/app.css

@ -202,7 +202,7 @@ @@ -202,7 +202,7 @@
@apply text-base font-semibold;
}
/* Heading links - primary-600 (light) / primary-400 (dark) for hover */
/* Heading links - primary-600 (light, more golden) / primary-300 (dark) for hover */
h1 a,
h2 a,
h3 a,
@ -216,7 +216,7 @@ @@ -216,7 +216,7 @@
h5.h-leather a,
h6.h-leather a {
@apply text-gray-900 dark:text-gray-100 hover:text-primary-600
dark:hover:text-primary-400;
dark:hover:text-primary-300;
}
/* === LEATHER COMPONENTS === */
@ -225,6 +225,39 @@ @@ -225,6 +225,39 @@
@apply text-gray-900 dark:text-gray-100;
}
/* Override Flowbite button hover in light mode - make it subtle like nav bar */
/* Target all buttons in light mode */
button:hover {
background-color: var(--color-primary-100) !important;
}
/* Override outline buttons specifically */
button[class*="outline"]:hover {
background-color: var(--color-primary-100) !important;
border-color: var(--color-primary-200) !important;
color: var(--color-primary-800) !important;
}
/* Override btn-leather buttons */
button.btn-leather:hover {
background-color: var(--color-primary-100) !important;
}
/* Dark mode overrides */
.dark button:hover {
background-color: var(--color-primary-800) !important;
}
.dark button[class*="outline"]:hover {
background-color: var(--color-primary-800) !important;
border-color: var(--color-primary-600) !important;
color: var(--color-primary-100) !important;
}
.dark button.btn-leather:hover {
background-color: var(--color-primary-800) !important;
}
.btn-leather.text-xs {
@apply px-2 py-1;
}
@ -242,8 +275,8 @@ @@ -242,8 +275,8 @@
}
div[role="tooltip"] button.btn-leather {
@apply hover:text-primary-600 dark:hover:text-primary-400
hover:border-primary-600 dark:hover:border-primary-400 hover:bg-gray-200
@apply hover:text-primary-700 dark:hover:text-primary-300
hover:border-primary-700 dark:hover:border-primary-300 hover:bg-gray-200
dark:hover:bg-gray-700;
}
@ -301,7 +334,7 @@ @@ -301,7 +334,7 @@
div.modal-leather > div > h4 a,
div.modal-leather > div > h5 a,
div.modal-leather > div > h6 a {
@apply hover:text-primary-600 dark:hover:text-primary-400;
@apply hover:text-primary-700 dark:hover:text-primary-300;
}
/* Navbar */
@ -314,17 +347,70 @@ @@ -314,17 +347,70 @@
}
nav.navbar-leather svg {
@apply fill-gray-900 hover:fill-primary-600 dark:fill-gray-100
dark:hover:fill-primary-400;
@apply fill-gray-900 hover:fill-primary-700 dark:fill-gray-100
dark:hover:fill-primary-300;
}
/* NavBrand hover - all text highlights together */
#navi a:hover h1,
#navi a:hover p {
@apply !text-primary-600 dark:!text-primary-400;
@apply !text-primary-600 dark:!text-primary-300;
transition: color 0.2s ease-in-out;
}
/* Navbar menu items hover effect - ALL items get same background hover */
#navi ul li.navbar-menu-item,
#navi ul li:has(.navbar-menu-item),
#navi li.navbar-menu-item {
@apply rounded px-2 py-1 transition-colors;
}
#navi ul li.navbar-menu-item:hover,
#navi ul li:has(.navbar-menu-item):hover,
#navi li.navbar-menu-item:hover,
#navi ul li.navbar-menu-item:has(button:hover),
#navi ul li.navbar-menu-item:has(div:hover),
#navi ul li.navbar-menu-item:has(span:hover),
#navi ul li.navbar-menu-item:has(a:hover),
#navi ul li.navbar-menu-item:has(img:hover),
#navi ul li.navbar-menu-item:has(svg:hover),
#navi ul li.navbar-menu-item:has([class*="Avatar"]:hover) {
@apply !bg-primary-100;
}
.dark #navi ul li.navbar-menu-item:hover,
.dark #navi ul li:has(.navbar-menu-item):hover,
.dark #navi li.navbar-menu-item:hover,
.dark #navi ul li.navbar-menu-item:has(button:hover),
.dark #navi ul li.navbar-menu-item:has(div:hover),
.dark #navi ul li.navbar-menu-item:has(span:hover),
.dark #navi ul li.navbar-menu-item:has(a:hover),
.dark #navi ul li.navbar-menu-item:has(img:hover),
.dark #navi ul li.navbar-menu-item:has(svg:hover),
.dark #navi ul li.navbar-menu-item:has([class*="Avatar"]:hover) {
@apply !bg-primary-800;
}
/* Explore text color - matches chevron in dark mode */
#navi ul li.navbar-menu-item:first-of-type {
@apply text-primary-800 dark:text-white cursor-pointer;
}
/* Remove ALL backgrounds from ALL child elements - use universal selector with max specificity */
#navi ul li.navbar-menu-item *,
#navi ul li.navbar-menu-item *:hover,
#navi ul li.navbar-menu-item *:focus,
#navi ul li.navbar-menu-item *:active,
#navi ul li.navbar-menu-item:hover *,
#navi ul li.navbar-menu-item:hover *:hover,
#navi ul li.navbar-menu-item:hover *:focus,
#navi ul li.navbar-menu-item:hover *:active {
background-color: transparent !important;
background: transparent !important;
background-image: none !important;
box-shadow: none !important;
}
nav.navbar-leather h1,
nav.navbar-leather h2,
nav.navbar-leather h3,
@ -340,7 +426,7 @@ @@ -340,7 +426,7 @@
nav.navbar-leather h4 a,
nav.navbar-leather h5 a,
nav.navbar-leather h6 a {
@apply hover:text-primary-600 dark:hover:text-primary-400;
@apply hover:text-primary-700 dark:hover:text-primary-300;
}
div.textarea-leather {
@ -430,23 +516,23 @@ @@ -430,23 +516,23 @@
/* Lists */
.ol-leather li a,
.ul-leather li a {
@apply text-gray-900 dark:text-gray-100 hover:text-primary-600
dark:hover:text-primary-400;
@apply text-gray-900 dark:text-gray-100 hover:text-primary-700
dark:hover:text-primary-300;
}
/* Links - consistent hover colors */
/* Links - consistent hover colors - improved contrast */
.link {
@apply underline cursor-pointer hover:text-primary-600
dark:hover:text-primary-400;
@apply underline cursor-pointer hover:text-primary-700
dark:hover:text-primary-300;
}
.npub-badge {
@apply inline-flex space-x-1 items-center text-primary-600
dark:text-primary-500 hover:underline me-2 px-2 py-0.5 rounded-sm border
border-primary-600 dark:border-primary-500;
@apply inline-flex space-x-1 items-center text-primary-700
dark:text-primary-300 hover:underline me-2 px-2 py-0.5 rounded-sm border
border-primary-700 dark:border-primary-300;
svg {
@apply fill-primary-600 dark:fill-primary-500;
@apply fill-primary-700 dark:fill-primary-300;
}
}
@ -455,6 +541,19 @@ @@ -455,6 +541,19 @@
}
}
/* Force remove backgrounds from DarkMode button - outside layer for max priority */
#navi ul li.navbar-menu-item:nth-child(2) *,
#navi ul li.navbar-menu-item:nth-child(2) *:hover,
#navi ul li.navbar-menu-item:nth-child(2) *:focus,
#navi ul li.navbar-menu-item:nth-child(2) *:active,
#navi ul li.navbar-menu-item:nth-child(2):hover *,
#navi ul li.navbar-menu-item:nth-child(2):hover *:hover {
background-color: transparent !important;
background: transparent !important;
background-image: none !important;
box-shadow: none !important;
}
@layer components {
nav a {
text-decoration-line: none !important;
@ -464,6 +563,23 @@ @@ -464,6 +563,23 @@
@apply block mx-auto my-4;
}
/* Fix white wrapper behind buttons on publication content in light mode */
main.publication div.flex.gap-2,
main.publication div.flex.justify-between {
@apply bg-transparent;
}
/* Override Flowbite light button white background in light mode to be more subtle */
main.publication :global(button.bg-gray-100),
main.publication :global(button.bg-gray-50) {
@apply !bg-primary-100 !border-primary-200 !text-primary-800;
}
main.publication :global(button.bg-gray-100:hover),
main.publication :global(button.bg-gray-50:hover) {
@apply !bg-primary-200 !border-primary-300;
}
/* Legend */
.leather-legend {
@apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2
@ -488,7 +604,7 @@ @@ -488,7 +604,7 @@
}
.leather-legend button {
@apply dark:text-white;
@apply text-gray-900 dark:text-gray-100;
}
.publication-leather {
@ -527,10 +643,10 @@ @@ -527,10 +643,10 @@
}
}
/* All links - consistent hover behavior */
/* All links - consistent hover behavior - improved contrast */
a {
@apply underline cursor-pointer hover:text-primary-600
dark:hover:text-primary-400;
@apply underline cursor-pointer hover:text-primary-700
dark:hover:text-primary-300;
}
.imageblock {
@ -567,10 +683,14 @@ @@ -567,10 +683,14 @@
}
}
/* Footnotes */
/* Footnotes - improved contrast */
.footnote-ref {
text-decoration: none;
color: var(--color-primary-500);
color: var(--color-primary-700);
}
.dark .footnote-ref {
color: var(--color-primary-300);
}
.footnotes {
@ -600,12 +720,21 @@ @@ -600,12 +720,21 @@
.footnote-backref {
text-decoration: none;
margin-left: 0.5rem;
color: var(--color-primary-500);
color: var(--color-primary-700);
}
.dark .footnote-backref {
color: var(--color-primary-300);
}
.note-leather .footnote-ref,
.note-leather .footnote-backref {
color: var(--color-primary-500);
color: var(--color-primary-700);
}
.dark .note-leather .footnote-ref,
.dark .note-leather .footnote-backref {
color: var(--color-primary-300);
}
/* Scrollable content */
@ -678,10 +807,11 @@ @@ -678,10 +807,11 @@
@apply focus:border-primary-600 dark:focus:border-primary-400;
}
/* Table of Contents highlighting */
/* Table of Contents highlighting - improved contrast */
.toc-highlight {
@apply bg-primary-300 dark:bg-primary-700 border-s-4 border-primary-600
rounded dark:border-primary-400 font-medium;
@apply bg-primary-300 dark:bg-primary-700 border-s-4 border-primary-700
rounded dark:border-primary-300 font-medium text-gray-900
dark:text-gray-100;
transition: all 0.2s ease-in-out;
}

8
src/app.html

@ -8,8 +8,12 @@ @@ -8,8 +8,12 @@
<!-- Apply saved theme ASAP to avoid flash -->
<script>
try {
const t = localStorage.getItem("theme");
if (t) document.documentElement.dataset.theme = t;
const t = localStorage.getItem("alexandria/theme") || "light";
document.documentElement.dataset.theme = t;
// Add .dark class for non-light themes
if (t !== "light") {
document.documentElement.classList.add("dark");
}
} catch (_) {
/* no-op */
}

12
src/lib/a/nav/ANavbar.svelte

@ -121,11 +121,10 @@ @@ -121,11 +121,10 @@
</div>
</NavBrand>
<div class="flex md:order-2">
<Profile />
<NavHamburger />
</div>
<NavUl class="order-1 ml-auto items-center" classes={{ ul: "items-center" }}>
<NavLi class="cursor-pointer">
<NavLi class="navbar-menu-item">
Explore<ChevronDownOutline
class="text-primary-800 ms-2 inline h-6 w-6 dark:text-white"
/>
@ -134,7 +133,7 @@ @@ -134,7 +133,7 @@
{#snippet children({ item })}
<a
href={item.href}
class="block h-full rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700"
class="block h-full rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-primary-800 transition-colors"
>
<div class="font-semibold dark:text-white">{item.name}</div>
<span class="text-sm font-light text-gray-500 dark:text-gray-400"
@ -143,6 +142,11 @@ @@ -143,6 +142,11 @@
</a>
{/snippet}
</MegaMenu>
<DarkMode />
<NavLi class="navbar-menu-item">
<DarkMode />
</NavLi>
<NavLi class="navbar-menu-item">
<Profile />
</NavLi>
</NavUl>
</Navbar>

3
src/lib/a/primitives/ADetails.svelte

@ -57,6 +57,7 @@ @@ -57,6 +57,7 @@
defaultOpen = false,
forceHide = false,
class: className = "",
children,
} = $props();
let open = $derived(defaultOpen);
$effect(() => {
@ -98,6 +99,6 @@ @@ -98,6 +99,6 @@
{#if !(tech && !$showTech && forceHide)}<div
class="px-3 pb-3 pt-1 text-[0.95rem] leading-6"
>
<slot />
{@render children()}
</div>{/if}
</details>

11
src/lib/a/primitives/ANostrUser.svelte

@ -96,6 +96,7 @@ @@ -96,6 +96,7 @@
badgeLimit = 6,
href = undefined as string | undefined,
class: className = "",
badges,
} = $props();
// Derived view-model
@ -207,11 +208,11 @@ @@ -207,11 +208,11 @@
{#if showBadges}
<span class="mt-1 block">
<slot name="badges">
{#if nativeBadges}
<ANostrBadgeRow badges={nativeBadges} limit={badgeLimit} size="s" />
{/if}
</slot>
{#if badges}
{@render badges()}
{:else if nativeBadges}
<ANostrBadgeRow badges={nativeBadges} limit={badgeLimit} size="s" />
{/if}
</span>
{/if}
</span>

48
src/lib/components/Navigation.svelte

@ -1,48 +0,0 @@ @@ -1,48 +0,0 @@
<script lang="ts">
import {
DarkMode,
Navbar,
NavLi,
NavUl,
NavHamburger,
NavBrand,
} from "flowbite-svelte";
import Profile from "./util/Profile.svelte";
import { userStore } from "$lib/stores/userStore";
let { class: className = "" } = $props();
let userState = $derived($userStore);
</script>
<Navbar class={`Navbar navbar-leather navbar-main ${className}`}>
<div class="flex flex-grow justify-between">
<NavBrand href="/">
<div class="flex flex-col">
<h1 class="text-2xl font-bold">Alexandria</h1>
<p class="text-xs font-semibold tracking-wide max-sm:max-w-[11rem]">
READ THE ORIGINAL. MAKE CONNECTIONS. CULTIVATE KNOWLEDGE.
</p>
</div>
</NavBrand>
</div>
<div class="flex md:order-2">
<Profile />
<NavHamburger class="btn-leather" />
</div>
<NavUl class="ul-leather">
<NavLi href="/">Publications</NavLi>
<NavLi href="/new/compose">Compose</NavLi>
<NavLi href="/visualize">Visualize</NavLi>
<NavLi href="/start">Getting Started</NavLi>
<NavLi href="/events">Events</NavLi>
{#if userState.signedIn}
<NavLi href="/my-notes">My Notes</NavLi>
{/if}
<NavLi href="/about">About</NavLi>
<NavLi href="/contact">Contact</NavLi>
<NavLi>
<DarkMode class="btn-leather p-0" />
</NavLi>
</NavUl>
</Navbar>

137
src/lib/components/cards/BlogHeader.svelte

@ -87,60 +87,103 @@ @@ -87,60 +87,103 @@
</script>
{#if title != null}
<Card
class="ArticleBox card-leather w-full grid max-w-xl {active
? 'active'
: ''}"
>
<div class="space-y-4 relative">
<div class="flex flex-row justify-between my-2">
<div class="flex flex-col">
{@render userBadge(authorPubkey, author, ndk)}
<span class="text-gray-700 dark:text-gray-300">{publishedAt()}</span>
{#if active}
<!-- Full card view when active -->
<div
class="ArticleBox card-leather w-full grid active cursor-pointer min-w-0"
role="button"
tabindex={0}
onclick={(e: MouseEvent) => {
// Don't trigger if clicking on CardActions or its children
const target = e.target as HTMLElement;
if (target.closest('.card-actions') || target.closest('button[type="button"]')) {
return;
}
showBlog();
}}
onkeydown={(e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
const target = e.target as HTMLElement;
if (!target.closest('.card-actions') && !target.closest('button[type="button"]')) {
showBlog();
}
}
}}
>
<Card class="w-full h-full min-w-0 !max-w-none">
<div class="space-y-4 relative pl-4 min-w-0 w-full">
<div class="flex flex-row justify-between my-2">
<div class="flex flex-col">
{@render userBadge(authorPubkey, author, ndk)}
<span class="text-gray-700 dark:text-gray-300">{publishedAt()}</span>
</div>
</div>
</div>
<div
class="ArticleBoxImage flex justify-center items-center p-2 h-40 -mt-2"
in:scale={{ start: 0.8, duration: 500, delay: 100, easing: quintOut }}
>
{#if image}
<LazyImage
src={image}
alt={title || "Publication image"}
eventId={event.id}
className="rounded w-full h-full object-cover"
/>
{:else}
<div
class="rounded w-full h-full"
style="background-color: {generateDarkPastelColor(event.id)};"
>
</div>
{/if}
</div>
<div
class="ArticleBoxImage flex justify-center items-center p-2 h-40 -mt-2"
in:scale={{ start: 0.8, duration: 500, delay: 100, easing: quintOut }}
>
{#if image}
<LazyImage
src={image}
alt={title || "Publication image"}
eventId={event.id}
className="rounded w-full h-full object-cover"
/>
{:else}
<div
class="rounded w-full h-full"
style="background-color: {generateDarkPastelColor(event.id)};"
>
</div>
{/if}
</div>
<div class="flex flex-col space-y-4">
<button onclick={() => showBlog()} class="text-left">
<div class="flex flex-col space-y-4">
<h2 class="text-lg font-bold line-clamp-2" {title}>{title}</h2>
</button>
{#if hashtags}
<div class="tags">
{#each hashtags as tag}
<span class="mr-2">#{tag}</span>
{/each}
</div>
{/if}
</div>
{#if hashtags}
<div class="tags">
{#each hashtags as tag}
<span>#{tag}</span>
{/each}
</div>
{/if}
</div>
{#if active}
<Interactions {rootId} {event} />
{/if}
<!-- Position CardActions at bottom-right -->
<div class="absolute bottom-2 right-2">
<CardActions {event} onDelete={handleDelete} />
<!-- Position CardActions at bottom-right -->
<div
class="absolute bottom-2 right-2 card-actions"
role="button"
tabindex={0}
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
>
<CardActions {event} onDelete={handleDelete} />
</div>
</div>
</Card>
</div>
{:else}
<!-- Simple list view when collapsed -->
<div
class="py-2 pl-4 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
role="button"
tabindex="0"
onclick={() => showBlog()}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
showBlog();
}
}}
>
<h3 class="text-base font-medium text-gray-900 dark:text-white">{title}</h3>
<div class="flex items-center gap-2 mt-1">
<p class="text-sm text-gray-500 dark:text-gray-400">{publishedAt()}</p>
<span class="text-sm text-gray-400 dark:text-gray-500"></span>
<p class="text-sm text-gray-500 dark:text-gray-400">{author}</p>
</div>
</div>
</Card>
{/if}
{/if}

14
src/lib/components/publications/CommentButton.svelte

@ -129,12 +129,6 @@ @@ -129,12 +129,6 @@
commentEvent.tags.push(["e", eventId, relayHint]);
}
console.log("[CommentButton] Created NIP-22 comment event:", {
kind: commentEvent.kind,
tags: commentEvent.tags,
content: commentEvent.content,
});
return commentEvent;
}
@ -179,8 +173,6 @@ @@ -179,8 +173,6 @@
await commentEvent.sign($userStore.signer);
}
console.log("[CommentButton] Signed comment event:", commentEvent.rawEvent());
// Build relay list following the same pattern as eventServices
const relays = [
...communityRelays,
@ -191,8 +183,6 @@ @@ -191,8 +183,6 @@
// Remove duplicates
const uniqueRelays = Array.from(new Set(relays));
console.log("[CommentButton] Publishing to relays:", uniqueRelays);
const signedEvent = {
...plainEvent,
id: commentEvent.id,
@ -217,11 +207,9 @@ @@ -217,11 +207,9 @@
clearTimeout(timeout);
if (ok) {
publishedCount++;
console.log(`[CommentButton] Published to ${relayUrl}`);
WebSocketPool.instance.release(ws);
resolve();
} else {
console.warn(`[CommentButton] ${relayUrl} rejected: ${message}`);
WebSocketPool.instance.release(ws);
reject(new Error(message));
}
@ -240,8 +228,6 @@ @@ -240,8 +228,6 @@
throw new Error("Failed to publish to any relays");
}
console.log(`[CommentButton] Published to ${publishedCount} relay(s)`);
// Success!
success = true;
commentContent = "";

109
src/lib/components/publications/CommentLayer.svelte

@ -36,7 +36,6 @@ @@ -36,7 +36,6 @@
async function fetchComments() {
// Prevent concurrent fetches
if (loading) {
console.log("[CommentLayer] Already loading, skipping fetch");
return;
}
@ -55,8 +54,6 @@ @@ -55,8 +54,6 @@
// AI-NOTE: Mock mode allows testing comment UI without publishing to relays
// This is useful for development and demonstrating the comment system
if (useMockComments) {
console.log(`[CommentLayer] MOCK MODE - Generating mock comments for ${allAddresses.length} sections`);
try {
// Generate mock comment data
const mockComments = generateMockCommentsForSections(allAddresses);
@ -64,7 +61,6 @@ @@ -64,7 +61,6 @@
// Convert to NDKEvent instances (same as real events)
comments = mockComments.map(rawEvent => new NDKEventClass(ndk, rawEvent));
console.log(`[CommentLayer] Generated ${comments.length} mock comments`);
loading = false;
return;
} catch (err) {
@ -74,11 +70,6 @@ @@ -74,11 +70,6 @@
}
}
console.log(`[CommentLayer] Fetching comments for:`, {
eventIds: allEventIds,
addresses: allAddresses
});
try {
// Build filter for kind 1111 comment events
// IMPORTANT: Use only #a tags because filters are AND, not OR
@ -96,8 +87,6 @@ @@ -96,8 +87,6 @@
filter["#e"] = allEventIds;
}
console.log(`[CommentLayer] Fetching with filter:`, JSON.stringify(filter, null, 2));
// Build explicit relay set (same pattern as HighlightLayer)
const relays = [
...communityRelays,
@ -105,7 +94,6 @@ @@ -105,7 +94,6 @@
...$activeInboxRelays,
];
const uniqueRelays = Array.from(new Set(relays));
console.log(`[CommentLayer] Fetching from ${uniqueRelays.length} relays:`, uniqueRelays);
/**
* Use WebSocketPool with nostr-tools protocol instead of NDK
@ -124,31 +112,55 @@ @@ -124,31 +112,55 @@
*/
const subscriptionId = `comments-${Date.now()}`;
const receivedEventIds = new Set<string>();
let eoseCount = 0;
let responseCount = 0;
const totalRelays = uniqueRelays.length;
// AI-NOTE: Helper to check if all relays have responded and clear loading state early
const checkAllResponses = () => {
responseCount++;
if (responseCount >= totalRelays && loading) {
loading = false;
}
};
const fetchPromises = uniqueRelays.map(async (relayUrl) => {
try {
console.log(`[CommentLayer] Connecting to ${relayUrl}`);
const ws = await WebSocketPool.instance.acquire(relayUrl);
return new Promise<void>((resolve) => {
let released = false;
let resolved = false;
const releaseConnection = () => {
if (released) {
return;
}
released = true;
try {
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
}
ws.removeEventListener("message", messageHandler);
WebSocketPool.instance.release(ws);
} catch (err) {
console.error(`[CommentLayer] Error releasing connection to ${relayUrl}:`, err);
}
};
const safeResolve = () => {
if (!resolved) {
resolved = true;
checkAllResponses();
resolve();
}
};
const messageHandler = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data);
// Log ALL messages from relay.nostr.band for debugging
if (relayUrl.includes('relay.nostr.band')) {
console.log(`[CommentLayer] RAW message from ${relayUrl}:`, message);
}
if (message[0] === "EVENT" && message[1] === subscriptionId) {
const rawEvent = message[2];
console.log(`[CommentLayer] EVENT from ${relayUrl}:`, {
id: rawEvent.id,
kind: rawEvent.kind,
content: rawEvent.content.substring(0, 50),
tags: rawEvent.tags
});
// Avoid duplicates
if (!receivedEventIds.has(rawEvent.id)) {
@ -157,19 +169,11 @@ @@ -157,19 +169,11 @@
// Convert to NDKEvent
const ndkEvent = new NDKEventClass(ndk, rawEvent);
comments = [...comments, ndkEvent];
console.log(`[CommentLayer] Added comment, total now: ${comments.length}`);
}
} else if (message[0] === "EOSE" && message[1] === subscriptionId) {
eoseCount++;
console.log(`[CommentLayer] EOSE from ${relayUrl} (${eoseCount}/${uniqueRelays.length})`);
// Close subscription
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
ws.removeEventListener("message", messageHandler);
WebSocketPool.instance.release(ws);
resolve();
} else if (message[0] === "NOTICE") {
console.warn(`[CommentLayer] NOTICE from ${relayUrl}:`, message[1]);
// Close subscription and release connection
releaseConnection();
safeResolve();
}
} catch (err) {
console.error(`[CommentLayer] Error processing message from ${relayUrl}:`, err);
@ -180,41 +184,25 @@ @@ -180,41 +184,25 @@
// Send REQ
const req = ["REQ", subscriptionId, filter];
if (relayUrl.includes('relay.nostr.band')) {
console.log(`[CommentLayer] Sending REQ to ${relayUrl}:`, JSON.stringify(req));
} else {
console.log(`[CommentLayer] Sending REQ to ${relayUrl}`);
}
ws.send(JSON.stringify(req));
// Timeout per relay (5 seconds)
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
ws.removeEventListener("message", messageHandler);
WebSocketPool.instance.release(ws);
}
resolve();
releaseConnection();
safeResolve();
}, 5000);
});
} catch (err) {
console.error(`[CommentLayer] Error connecting to ${relayUrl}:`, err);
// Mark this relay as responded if connection fails
checkAllResponses();
}
});
// Wait for all relays to respond or timeout
await Promise.all(fetchPromises);
console.log(`[CommentLayer] Fetched ${comments.length} comments`);
if (comments.length > 0) {
console.log(`[CommentLayer] Comments summary:`, comments.map(c => ({
content: c.content.substring(0, 30) + "...",
address: c.tags.find(t => t[0] === "a")?.[1],
author: c.pubkey.substring(0, 8)
})));
}
await Promise.allSettled(fetchPromises);
// Ensure loading is cleared even if checkAllResponses didn't fire
loading = false;
} catch (err) {
@ -232,8 +220,6 @@ @@ -232,8 +220,6 @@
const currentCount = eventIds.length + eventAddresses.length;
const hasEventData = currentCount > 0;
console.log(`[CommentLayer] Event data effect - count: ${currentCount}, lastFetched: ${lastFetchedCount}, loading: ${loading}`);
// Only fetch if:
// 1. We have event data
// 2. The count has changed since last fetch
@ -246,7 +232,6 @@ @@ -246,7 +232,6 @@
// Debounce: wait 500ms for more events to arrive before fetching
fetchTimeout = setTimeout(() => {
console.log(`[CommentLayer] Event data stabilized at ${currentCount} events, fetching comments...`);
lastFetchedCount = currentCount;
fetchComments();
}, 500);
@ -264,8 +249,6 @@ @@ -264,8 +249,6 @@
* Public method to refresh comments (e.g., after creating a new one)
*/
export function refresh() {
console.log("[CommentLayer] Manual refresh triggered");
// Clear existing comments
comments = [];

255
src/lib/components/publications/HighlightLayer.svelte

@ -75,7 +75,6 @@ @@ -75,7 +75,6 @@
async function fetchHighlights() {
// Prevent concurrent fetches
if (loading) {
console.log("[HighlightLayer] Already loading, skipping fetch");
return;
}
@ -99,10 +98,6 @@ @@ -99,10 +98,6 @@
// AI-NOTE: Mock mode allows testing highlight UI without publishing to relays
// This is useful for development and demonstrating the highlight system
if (useMockHighlights) {
console.log(
`[HighlightLayer] MOCK MODE - Generating mock highlights for ${allAddresses.length} sections`,
);
try {
// Generate mock highlight data
const mockHighlights = generateMockHighlightsForSections(allAddresses);
@ -112,9 +107,6 @@ @@ -112,9 +107,6 @@
(rawEvent) => new NDKEventClass(ndk, rawEvent),
);
console.log(
`[HighlightLayer] Generated ${highlights.length} mock highlights`,
);
loading = false;
return;
} catch (err) {
@ -127,11 +119,6 @@ @@ -127,11 +119,6 @@
}
}
console.log(`[HighlightLayer] Fetching highlights for:`, {
eventIds: allEventIds,
addresses: allAddresses,
});
try {
// Build filter for kind 9802 highlight events
// IMPORTANT: Use only #a tags because filters are AND, not OR
@ -149,11 +136,6 @@ @@ -149,11 +136,6 @@
filter["#e"] = allEventIds;
}
console.log(
`[HighlightLayer] Fetching with filter:`,
JSON.stringify(filter, null, 2),
);
// Build explicit relay set (same pattern as HighlightSelectionHandler and CommentButton)
const relays = [
...communityRelays,
@ -161,10 +143,6 @@ @@ -161,10 +143,6 @@
...$activeInboxRelays,
];
const uniqueRelays = Array.from(new Set(relays));
console.log(
`[HighlightLayer] Fetching from ${uniqueRelays.length} relays:`,
uniqueRelays,
);
/**
* Use WebSocketPool with nostr-tools protocol instead of NDK
@ -186,30 +164,41 @@ @@ -186,30 +164,41 @@
const fetchPromises = uniqueRelays.map(async (relayUrl) => {
try {
console.log(`[HighlightLayer] Connecting to ${relayUrl}`);
const ws = await WebSocketPool.instance.acquire(relayUrl);
return new Promise<void>((resolve) => {
let released = false;
let resolved = false;
const releaseConnection = () => {
if (released) {
return;
}
released = true;
try {
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
}
ws.removeEventListener("message", messageHandler);
WebSocketPool.instance.release(ws);
} catch (err) {
console.error(`[HighlightLayer] Error releasing connection to ${relayUrl}:`, err);
}
};
const safeResolve = () => {
if (!resolved) {
resolved = true;
resolve();
}
};
const messageHandler = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data);
// Log ALL messages from relay.nostr.band for debugging
if (relayUrl.includes("relay.nostr.band")) {
console.log(
`[HighlightLayer] RAW message from ${relayUrl}:`,
message,
);
}
if (message[0] === "EVENT" && message[1] === subscriptionId) {
const rawEvent = message[2];
console.log(`[HighlightLayer] EVENT from ${relayUrl}:`, {
id: rawEvent.id,
kind: rawEvent.kind,
content: rawEvent.content.substring(0, 50),
tags: rawEvent.tags,
});
// Avoid duplicates
if (!receivedEventIds.has(rawEvent.id)) {
@ -218,29 +207,16 @@ @@ -218,29 +207,16 @@
// Convert to NDKEvent
const ndkEvent = new NDKEventClass(ndk, rawEvent);
highlights = [...highlights, ndkEvent];
console.log(
`[HighlightLayer] Added highlight, total now: ${highlights.length}`,
);
}
} else if (
message[0] === "EOSE" &&
message[1] === subscriptionId
) {
eoseCount++;
console.log(
`[HighlightLayer] EOSE from ${relayUrl} (${eoseCount}/${uniqueRelays.length})`,
);
// Close subscription
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
ws.removeEventListener("message", messageHandler);
WebSocketPool.instance.release(ws);
resolve();
} else if (message[0] === "NOTICE") {
console.warn(
`[HighlightLayer] NOTICE from ${relayUrl}:`,
message[1],
);
// Close subscription and release connection
releaseConnection();
safeResolve();
}
} catch (err) {
console.error(
@ -254,24 +230,12 @@ @@ -254,24 +230,12 @@
// Send REQ
const req = ["REQ", subscriptionId, filter];
if (relayUrl.includes("relay.nostr.band")) {
console.log(
`[HighlightLayer] Sending REQ to ${relayUrl}:`,
JSON.stringify(req),
);
} else {
console.log(`[HighlightLayer] Sending REQ to ${relayUrl}`);
}
ws.send(JSON.stringify(req));
// Timeout per relay (5 seconds)
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(["CLOSE", subscriptionId]));
ws.removeEventListener("message", messageHandler);
WebSocketPool.instance.release(ws);
}
resolve();
releaseConnection();
safeResolve();
}, 5000);
});
} catch (err) {
@ -285,19 +249,6 @@ @@ -285,19 +249,6 @@
// Wait for all relays to respond or timeout
await Promise.all(fetchPromises);
console.log(`[HighlightLayer] Fetched ${highlights.length} highlights`);
if (highlights.length > 0) {
console.log(
`[HighlightLayer] Highlights summary:`,
highlights.map((h) => ({
content: h.content.substring(0, 30) + "...",
address: h.tags.find((t) => t[0] === "a")?.[1],
author: h.pubkey.substring(0, 8),
})),
);
}
loading = false;
// Rendering is handled by the visibility/highlights effect
@ -321,9 +272,6 @@ @@ -321,9 +272,6 @@
targetAddress?: string,
): boolean {
if (!containerRef) {
console.log(
`[HighlightLayer] Cannot highlight by position - no containerRef`,
);
return false;
}
@ -333,30 +281,10 @@ @@ -333,30 +281,10 @@
const sectionElement = document.getElementById(targetAddress);
if (sectionElement) {
searchRoot = sectionElement;
console.log(
`[HighlightLayer] Highlighting in specific section: ${targetAddress}`,
);
} else {
console.log(
`[HighlightLayer] Section ${targetAddress} not found in DOM, searching globally`,
);
}
}
console.log(
`[HighlightLayer] Applying position-based highlight ${offsetStart}-${offsetEnd}`,
);
const result = highlightByOffset(searchRoot, offsetStart, offsetEnd, color);
if (result) {
console.log(
`[HighlightLayer] Successfully applied position-based highlight`,
);
} else {
console.log(`[HighlightLayer] Failed to apply position-based highlight`);
}
return result;
return highlightByOffset(searchRoot, offsetStart, offsetEnd, color);
}
/**
@ -371,9 +299,6 @@ @@ -371,9 +299,6 @@
targetAddress?: string,
): void {
if (!containerRef || !text || text.trim().length === 0) {
console.log(
`[HighlightLayer] Cannot highlight - containerRef: ${!!containerRef}, text: "${text}"`,
);
return;
}
@ -383,21 +308,9 @@ @@ -383,21 +308,9 @@
const sectionElement = document.getElementById(targetAddress);
if (sectionElement) {
searchRoot = sectionElement;
console.log(
`[HighlightLayer] Searching in specific section: ${targetAddress}`,
);
} else {
console.log(
`[HighlightLayer] Section ${targetAddress} not found in DOM, searching globally`,
);
}
}
console.log(
`[HighlightLayer] Searching for text: "${text}" in`,
searchRoot,
);
// Use TreeWalker to find all text nodes
const walker = document.createTreeWalker(
searchRoot,
@ -412,22 +325,11 @@ @@ -412,22 +325,11 @@
}
// Search for the highlight text in text nodes
console.log(
`[HighlightLayer] Searching through ${textNodes.length} text nodes`,
);
for (const textNode of textNodes) {
const nodeText = textNode.textContent || "";
const index = nodeText.toLowerCase().indexOf(text.toLowerCase());
if (index !== -1) {
console.log(
`[HighlightLayer] Found match in text node:`,
nodeText.substring(
Math.max(0, index - 20),
Math.min(nodeText.length, index + text.length + 20),
),
);
const parent = textNode.parentNode;
if (!parent) continue;
@ -459,44 +361,26 @@ @@ -459,44 +361,26 @@
parent.replaceChild(fragment, textNode);
console.log(`[HighlightLayer] Highlighted text:`, match);
return; // Only highlight first occurrence to avoid multiple highlights
}
}
console.log(`[HighlightLayer] No match found for text: "${text}"`);
}
/**
* Render all highlights on the page
*/
function renderHighlights() {
console.log(
`[HighlightLayer] renderHighlights called - visible: ${visible}, containerRef: ${!!containerRef}, highlights: ${highlights.length}`,
);
if (!visible || !containerRef) {
console.log(
`[HighlightLayer] Skipping render - visible: ${visible}, containerRef: ${!!containerRef}`,
);
return;
}
if (highlights.length === 0) {
console.log(`[HighlightLayer] No highlights to render`);
return;
}
// Clear existing highlights
clearHighlights();
console.log(`[HighlightLayer] Rendering ${highlights.length} highlights`);
console.log(`[HighlightLayer] Container element:`, containerRef);
console.log(
`[HighlightLayer] Container has children:`,
containerRef.children.length,
);
// Apply each highlight
for (const highlight of highlights) {
const content = highlight.content;
@ -511,42 +395,19 @@ @@ -511,42 +395,19 @@
const hasOffset =
offsetTag && offsetTag[1] !== undefined && offsetTag[2] !== undefined;
console.log(`[HighlightLayer] Rendering highlight:`, {
hasOffset,
offsetTag,
content: content.substring(0, 50),
contentLength: content.length,
targetAddress,
color,
allTags: highlight.tags,
});
if (hasOffset) {
// Use position-based highlighting
const offsetStart = parseInt(offsetTag[1], 10);
const offsetEnd = parseInt(offsetTag[2], 10);
if (!isNaN(offsetStart) && !isNaN(offsetEnd)) {
console.log(
`[HighlightLayer] Using position-based highlighting: ${offsetStart}-${offsetEnd}`,
);
highlightByPosition(offsetStart, offsetEnd, color, targetAddress);
} else {
console.log(
`[HighlightLayer] Invalid offset values, falling back to text search`,
);
if (content && content.trim().length > 0) {
findAndHighlightText(content, color, targetAddress);
}
}
} else {
// Fall back to text-based highlighting
console.log(`[HighlightLayer] Using text-based highlighting`);
if (content && content.trim().length > 0) {
} else if (content && content.trim().length > 0) {
findAndHighlightText(content, color, targetAddress);
} else {
console.log(`[HighlightLayer] Skipping highlight - empty content`);
}
} else if (content && content.trim().length > 0) {
// Fall back to text-based highlighting
findAndHighlightText(content, color, targetAddress);
}
}
@ -575,10 +436,6 @@ @@ -575,10 +436,6 @@
parent.normalize();
}
});
console.log(
`[HighlightLayer] Cleared ${highlightElements.length} highlights`,
);
}
// Track the last fetched event count to know when to refetch
@ -590,10 +447,6 @@ @@ -590,10 +447,6 @@
const currentCount = eventIds.length + eventAddresses.length;
const hasEventData = currentCount > 0;
console.log(
`[HighlightLayer] Event data effect - count: ${currentCount}, lastFetched: ${lastFetchedCount}, loading: ${loading}`,
);
// Only fetch if:
// 1. We have event data
// 2. The count has changed since last fetch
@ -606,9 +459,6 @@ @@ -606,9 +459,6 @@
// Debounce: wait 500ms for more events to arrive before fetching
fetchTimeout = setTimeout(() => {
console.log(
`[HighlightLayer] Event data stabilized at ${currentCount} events, fetching highlights...`,
);
lastFetchedCount = currentCount;
fetchHighlights();
}, 500);
@ -626,14 +476,8 @@ @@ -626,14 +476,8 @@
$effect(() => {
// This effect runs when either visible or highlights.length changes
const highlightCount = highlights.length;
console.log(
`[HighlightLayer] Visibility/highlights effect - visible: ${visible}, highlights: ${highlightCount}`,
);
if (visible && highlightCount > 0) {
console.log(
`[HighlightLayer] Both visible and highlights ready, rendering...`,
);
renderHighlights();
} else if (!visible) {
clearHighlights();
@ -653,9 +497,6 @@ @@ -653,9 +497,6 @@
*/
async function fetchAuthorProfiles() {
const uniquePubkeys = Array.from(groupedHighlights.keys());
console.log(
`[HighlightLayer] Fetching profiles for ${uniquePubkeys.length} authors`,
);
for (const pubkey of uniquePubkeys) {
try {
@ -693,27 +534,17 @@ @@ -693,27 +534,17 @@
* Scroll to a specific highlight in the document
*/
function scrollToHighlight(highlight: NDKEvent) {
console.log(
`[HighlightLayer] scrollToHighlight called for:`,
highlight.content.substring(0, 50),
);
if (!containerRef) {
console.warn(`[HighlightLayer] No containerRef available`);
return;
}
const content = highlight.content;
if (!content || content.trim().length === 0) {
console.warn(`[HighlightLayer] No content in highlight`);
return;
}
// Find the highlight mark element
const highlightMarks = containerRef.querySelectorAll("mark.highlight");
console.log(
`[HighlightLayer] Found ${highlightMarks.length} highlight marks in DOM`,
);
// Try exact match first
for (const mark of highlightMarks) {
@ -721,9 +552,6 @@ @@ -721,9 +552,6 @@
const searchText = content.toLowerCase();
if (markText === searchText) {
console.log(
`[HighlightLayer] Found exact match, scrolling and flashing`,
);
// Scroll to this element
mark.scrollIntoView({ behavior: "smooth", block: "center" });
@ -742,9 +570,6 @@ @@ -742,9 +570,6 @@
const searchText = content.toLowerCase();
if (markText.includes(searchText) || searchText.includes(markText)) {
console.log(
`[HighlightLayer] Found partial match, scrolling and flashing`,
);
mark.scrollIntoView({ behavior: "smooth", block: "center" });
mark.classList.add("highlight-flash");
setTimeout(() => {
@ -753,11 +578,6 @@ @@ -753,11 +578,6 @@
return;
}
}
console.warn(
`[HighlightLayer] Could not find highlight mark for:`,
content.substring(0, 50),
);
}
/**
@ -770,7 +590,6 @@ @@ -770,7 +590,6 @@
try {
await navigator.clipboard.writeText(naddr);
copyFeedback = highlight.id;
console.log(`[HighlightLayer] Copied naddr to clipboard:`, naddr);
// Clear feedback after 2 seconds
setTimeout(() => {
@ -792,8 +611,6 @@ @@ -792,8 +611,6 @@
* Public method to refresh highlights (e.g., after creating a new one)
*/
export function refresh() {
console.log("[HighlightLayer] Manual refresh triggered");
// Clear existing highlights
highlights = [];
clearHighlights();

26
src/lib/components/publications/HighlightSelectionHandler.svelte

@ -97,7 +97,6 @@ @@ -97,7 +97,6 @@
// Don't use closest('.publication-leather') as Details also has that class
const publicationSection = target.closest("section[id]") as HTMLElement;
if (!publicationSection) {
console.log("[HighlightSelectionHandler] No section[id] found, aborting");
return;
}
@ -105,14 +104,6 @@ @@ -105,14 +104,6 @@
const sectionAddress = publicationSection.dataset.eventAddress;
const sectionEventId = publicationSection.dataset.eventId;
console.log("[HighlightSelectionHandler] Selection in section:", {
element: publicationSection,
address: sectionAddress,
eventId: sectionEventId,
allDataAttrs: publicationSection.dataset,
sectionId: publicationSection.id,
});
currentSelection = selection;
selectedText = text;
selectedSectionAddress = sectionAddress;
@ -155,12 +146,6 @@ @@ -155,12 +146,6 @@
selectedSectionAddress || publicationEvent.tagAddress();
const useEventId = selectedSectionEventId || publicationEvent.id;
console.log("[HighlightSelectionHandler] Creating highlight with:", {
address: useAddress,
eventId: useEventId,
fallbackUsed: !selectedSectionAddress,
});
const tags: string[][] = [];
// Always prefer addressable events for publications
@ -227,11 +212,6 @@ @@ -227,11 +212,6 @@
// Remove duplicates
const uniqueRelays = Array.from(new Set(relays));
console.log(
"[HighlightSelectionHandler] Publishing to relays:",
uniqueRelays,
);
const signedEvent = {
...plainEvent,
id: event.id,
@ -256,15 +236,9 @@ @@ -256,15 +236,9 @@
clearTimeout(timeout);
if (ok) {
publishedCount++;
console.log(
`[HighlightSelectionHandler] Published to ${relayUrl}`,
);
WebSocketPool.instance.release(ws);
resolve();
} else {
console.warn(
`[HighlightSelectionHandler] ${relayUrl} rejected: ${message}`,
);
WebSocketPool.instance.release(ws);
reject(new Error(message));
}

1298
src/lib/components/publications/Publication.svelte

File diff suppressed because it is too large Load Diff

6
src/lib/components/publications/PublicationHeader.svelte

@ -80,9 +80,9 @@ @@ -80,9 +80,9 @@
</script>
{#if title != null && href != null}
<Card class="ArticleBox max-h-52 card-leather w-full relative flex flex-col sm:flex-row sm:space-x-2 overflow-hidden">
<!-- Image block: full width on mobile, fixed side on md+ -->
<div class="w-full sm:min-w-40 sm:w-40 overflow-hidden flex items-center justify-center sm:rounded-l rounded-t sm:rounded-t-none">
<Card class="ArticleBox max-h-52 card-leather w-full relative flex flex-row space-x-2 overflow-hidden">
<!-- Image block: thumbnail on mobile, fixed side on sm+ -->
<div class="w-24 min-w-24 sm:min-w-40 sm:w-40 overflow-hidden flex items-center justify-center rounded-l">
{#if image}
<LazyImage
src={image}

90
src/lib/components/publications/PublicationSection.svelte

@ -26,6 +26,8 @@ @@ -26,6 +26,8 @@
ref,
allComments = [],
commentsVisible = true,
publicationTitle,
isFirstSection = false,
}: {
address: string;
rootAddress: string;
@ -35,6 +37,8 @@ @@ -35,6 +37,8 @@
ref: (ref: HTMLElement) => void;
allComments?: NDKEvent[];
commentsVisible?: boolean;
publicationTitle?: string;
isFirstSection?: boolean;
} = $props();
const asciidoctor: Asciidoctor = getContext("asciidoctor");
@ -59,10 +63,6 @@ @@ -59,10 +63,6 @@
leafEvent.then((e) => {
if (e?.id) {
leafEventId = e.id;
console.log(
`[PublicationSection] Set leafEventId for ${address}:`,
e.id,
);
}
});
});
@ -89,17 +89,32 @@ @@ -89,17 +89,32 @@
// AI-NOTE: Kind 30023 events contain Markdown content, not AsciiDoc
// Use parseAdvancedmarkup for 30023 events, Asciidoctor for 30041/30818 events
let processed: string;
if (event?.kind === 30023) {
return await parseAdvancedmarkup(content);
processed = await parseAdvancedmarkup(content);
} else {
// For 30041 and 30818 events, use Asciidoctor (AsciiDoc)
const converted = asciidoctor.convert(content);
const processed = await postProcessAdvancedAsciidoctorHtml(
processed = await postProcessAdvancedAsciidoctorHtml(
converted.toString(),
ndk,
);
return processed;
}
// Remove redundant h1 title from first section if it matches publication title
if (isFirstSection && publicationTitle && typeof processed === 'string') {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = processed;
const h1Elements = tempDiv.querySelectorAll('h1');
h1Elements.forEach((h1) => {
if (h1.textContent?.trim() === publicationTitle.trim()) {
h1.remove();
}
});
processed = tempDiv.innerHTML;
}
return processed;
});
let previousLeafEvent: NDKEvent | null = $derived.by(() => {
@ -186,11 +201,7 @@ @@ -186,11 +201,7 @@
eventAddress: address,
eventKind: event.kind,
reason: "User deleted section",
onSuccess: (deletionEventId) => {
console.log(
"[PublicationSection] Deletion event published:",
deletionEventId,
);
onSuccess: () => {
// Refresh the page to reflect the deletion
window.location.reload();
},
@ -212,19 +223,11 @@ @@ -212,19 +223,11 @@
}
ref(sectionRef);
// Log data attributes for debugging
console.log(`[PublicationSection] Section mounted:`, {
address,
leafEventId,
dataAddress: sectionRef.dataset.eventAddress,
dataEventId: sectionRef.dataset.eventId,
});
});
</script>
<!-- Wrapper for positioning context -->
<div class="relative w-full">
<div class="relative w-full overflow-x-hidden">
<section
id={address}
bind:this={sectionRef}
@ -235,20 +238,8 @@ @@ -235,20 +238,8 @@
{#await Promise.all( [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches], )}
<TextPlaceholder size="2xl" />
{:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]}
<!-- Main content area - centered -->
<div class="section-content relative max-w-4xl mx-auto px-4">
<!-- Mobile menu - shown only on smaller screens -->
<div class="xl:hidden absolute top-2 right-2 z-10">
{#await leafEvent then event}
{#if event}
<CardActions
{event}
sectionAddress={address}
onDelete={handleDelete}
/>
{/if}
{/await}
</div>
<!-- Main content area - left-aligned -->
<div class="section-content relative w-full text-left">
{#each divergingBranches as [branch, depth]}
{@render sectionHeading(
getMatchingTags(branch, "title")[0]?.[1] ?? "",
@ -257,7 +248,21 @@ @@ -257,7 +248,21 @@
{/each}
{#if leafTitle}
{@const leafDepth = leafHierarchy.length - 1}
{@render sectionHeading(leafTitle, leafDepth)}
<div class="relative">
<!-- Section actions button - positioned next to heading -->
<div class="absolute top-0 right-0 z-20">
{#await leafEvent then event}
{#if event}
<CardActions
{event}
sectionAddress={address}
onDelete={handleDelete}
/>
{/if}
{/await}
</div>
{@render sectionHeading(leafTitle, leafDepth)}
</div>
{/if}
{@render contentParagraph(
leafContent.toString(),
@ -267,7 +272,7 @@ @@ -267,7 +272,7 @@
</div>
<!-- Mobile comments - shown below content on smaller screens -->
<div class="xl:hidden mt-8 max-w-4xl mx-auto px-4">
<div class="xl:hidden mt-8 w-full text-left">
<SectionComments
sectionAddress={address}
comments={sectionComments}
@ -277,17 +282,6 @@ @@ -277,17 +282,6 @@
{/await}
</section>
<!-- Right sidebar elements - positioned very close to content, responsive width -->
{#await leafEvent then event}
{#if event}
<!-- Three-dot menu - positioned at top-center on XL+ screens -->
<div
class="hidden xl:block absolute left-[calc(50%+26rem)] top-[20%] z-10"
>
<CardActions {event} sectionAddress={address} onDelete={handleDelete} />
</div>
{/if}
{/await}
<!-- Comments area: positioned below menu, top-center of section -->
<div

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

@ -12,21 +12,47 @@ @@ -12,21 +12,47 @@
import Self from "./TableOfContents.svelte";
import { onMount, onDestroy } from "svelte";
let { depth, onSectionFocused, onLoadMore, toc } = $props<{
let { rootAddress, depth, onSectionFocused, onLoadMore, onClose, toc } = $props<{
rootAddress: string;
depth: number;
toc: TableOfContents;
onSectionFocused?: (address: string) => void;
onLoadMore?: () => void;
onClose?: () => void;
}>();
let entries = $derived.by<TocEntry[]>(() => {
const newEntries = [];
const rootEntry = rootAddress === toc.getRootEntry()?.address
? toc.getRootEntry()
: toc.getEntry(rootAddress);
if (!rootEntry) {
return [];
}
// Filter entries that are direct children of rootAddress at the correct depth
for (const [_, entry] of toc.addressMap) {
// Must match the depth
if (entry.depth !== depth) {
continue;
}
// Check if entry is a direct child of rootAddress
// Primary check: parent relationship (set when resolveChildren is called)
// Fallback: entry is in rootEntry's children array
// Final fallback: depth-based check for root's direct children only
const isDirectChild =
entry.parent?.address === rootAddress ||
rootEntry.children.some((child: TocEntry) => child.address === entry.address) ||
(entry.depth === rootEntry.depth + 1 &&
rootAddress === toc.getRootEntry()?.address &&
!entry.parent); // Only use depth check if parent not set (temporary state)
if (!isDirectChild) {
continue;
}
newEntries.push(entry);
}
@ -45,6 +71,36 @@ @@ -45,6 +71,36 @@
toc.expandedMap.set(address, expanded);
entry.resolveChildren();
// AI-NOTE: When expanding a chapter, scroll it to the top of the TOC so its children are visible
if (expanded) {
// Use setTimeout to allow the expansion animation to start and DOM to update
setTimeout(() => {
// Find the scrollable container (the div with overflow-y-auto in the TOC drawer)
const scrollableContainer = document.querySelector('.overflow-y-auto');
if (!scrollableContainer) {
return;
}
// Find all buttons in the TOC that match the entry title
const buttons = scrollableContainer.querySelectorAll('button');
for (const button of buttons) {
const buttonText = button.textContent?.trim();
if (buttonText === entry.title) {
// Find the parent container of the dropdown (the SidebarDropdownWrapper)
const dropdownContainer = button.closest('[class*="w-full"]');
if (dropdownContainer) {
// Scroll the chapter to the top of the TOC container
dropdownContainer.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
break;
}
}
}, 150);
}
}
function handleSectionClick(address: string) {
@ -59,11 +115,13 @@ @@ -59,11 +115,13 @@
onSectionFocused?.(address);
// Close the drawer after navigation
onClose?.();
// Check if this is the last entry and trigger loading more events
const currentEntries = entries;
const lastEntry = currentEntries[currentEntries.length - 1];
if (lastEntry && lastEntry.address === address) {
console.debug('[TableOfContents] Last entry clicked, triggering load more');
onLoadMore?.();
}
}
@ -73,6 +131,27 @@ @@ -73,6 +131,27 @@
return currentVisibleSection === address;
}
// Calculate indentation based on depth
// Depth 2 = no indent (Beginning, root entry)
// Depth 3 = indent level 1 (30041 sections under 30040)
// Depth 4+ = more indentation
function getIndentClass(depth: number): string {
if (depth <= 2) {
return "";
}
// Each level beyond 2 adds 1rem (16px) of padding
const indentLevel = depth - 2;
// Use standard Tailwind classes: pl-4 (1rem), pl-8 (2rem), pl-12 (3rem), etc.
const paddingMap: Record<number, string> = {
1: "pl-4", // 1rem
2: "pl-8", // 2rem
3: "pl-12", // 3rem
4: "pl-16", // 4rem
5: "pl-20", // 5rem
};
return paddingMap[indentLevel] || `pl-[${indentLevel}rem]`;
}
// Set up intersection observer to track visible sections
onMount(() => {
observer = new IntersectionObserver(
@ -153,17 +232,62 @@ @@ -153,17 +232,62 @@
<!-- TODO: Figure out how to style indentations. -->
<!-- TODO: Make group title fonts the same as entry title fonts. -->
<SidebarGroup>
<!-- Beginning entry - scrolls to top of page -->
{#if depth === 2}
<SidebarItem
label="Beginning"
href="#"
spanClass="px-2 text-ellipsis"
class={getIndentClass(2)}
onclick={(e) => {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth',
});
onClose?.();
}}
>
<!-- Beginning entry -->
</SidebarItem>
{/if}
<!-- Root entry (publication header) -->
{#if depth === 2}
{@const rootEntry = toc.getRootEntry()}
{#if rootEntry}
{@const isVisible = isEntryVisible(rootEntry.address)}
<SidebarItem
label={rootEntry.title}
href={`#${rootEntry.address}`}
spanClass="px-2 text-ellipsis"
class={`${getIndentClass(rootEntry.depth)} ${isVisible ? "toc-highlight" : ""} `}
onclick={() => {
const element = document.getElementById(rootEntry.address);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
onClose?.();
}}
>
<!-- Publication header entry -->
</SidebarItem>
{/if}
{/if}
{#each entries as entry, index}
{@const address = entry.address}
{@const expanded = toc.expandedMap.get(address) ?? false}
{@const isLeaf = toc.leaves.has(address)}
{@const isVisible = isEntryVisible(address)}
{@const indentClass = getIndentClass(entry.depth)}
{#if isLeaf}
<SidebarItem
label={entry.title}
href={`#${address}`}
spanClass="px-2 text-ellipsis"
class={`${isVisible ? "toc-highlight" : ""} `}
class={`${indentClass} ${isVisible ? "toc-highlight" : ""} `}
onclick={() => handleSectionClick(address)}
>
<!-- Empty for now - could add icons or labels here in the future -->
@ -172,10 +296,11 @@ @@ -172,10 +296,11 @@
{@const childDepth = depth + 1}
<SidebarDropdownWrapper
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 {isVisible ? 'toc-highlight' : ''} "
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' : ''} whitespace-nowrap min-w-fit {indentClass}"
class="w-full"
bind:isOpen={() => expanded, (open) => setEntryExpanded(address, open)}
>
<Self rootAddress={address} depth={childDepth} {toc} {onSectionFocused} {onLoadMore} />
<Self rootAddress={address} depth={childDepth} {toc} {onSectionFocused} {onLoadMore} {onClose} />
</SidebarDropdownWrapper>
{/if}
{/each}

62
src/lib/components/util/ArticleNav.svelte

@ -36,11 +36,14 @@ @@ -36,11 +36,14 @@
let lastScrollY = $state(0);
let isVisible = $state(true);
let navbarTop = $state(100); // Default to 100px
// Function to toggle column visibility
function toggleColumn(column: "toc" | "blog" | "inner" | "discussion") {
console.log("[ArticleNav] toggleColumn called with:", column);
publicationColumnVisibility.update((current) => {
const newValue = !current[column];
console.log("[ArticleNav] Toggling", column, "from", current[column], "to", newValue);
const updated = { ...current, [column]: newValue };
if (window.innerWidth < 1400 && column === "blog" && newValue) {
@ -93,6 +96,16 @@ @@ -93,6 +96,16 @@
});
}
function handleBlogTocClick() {
if ($publicationColumnVisibility.inner) {
// Viewing article: go back to TOC
backToBlog();
} else if ($publicationColumnVisibility.blog) {
// Showing TOC: toggle it (though it should stay visible)
toggleColumn("blog");
}
}
function handleScroll() {
if (window.innerWidth < 768) {
const currentScrollY = window.scrollY;
@ -139,6 +152,13 @@ @@ -139,6 +152,13 @@
let unsubscribe: () => void;
onMount(() => {
// Measure the actual navbar height to position ArticleNav correctly
const navbar = document.getElementById("navi");
if (navbar) {
const rect = navbar.getBoundingClientRect();
navbarTop = rect.bottom;
}
window.addEventListener("scroll", handleScroll);
unsubscribe = publicationColumnVisibility.subscribe(() => {
isVisible = true; // show navbar when store changes
@ -152,40 +172,38 @@ @@ -152,40 +172,38 @@
</script>
<nav
class="Navbar navbar-leather col-span-3 flex fixed top-[100px] sm:top-[92px] w-full min-h-[70px] px-2 sm:px-4 py-2.5 z-10 transition-transform duration-300 {isVisible
class="Navbar navbar-leather col-span-2 flex fixed w-full min-h-[70px] px-2 sm:px-4 py-2.5 z-10 transition-transform duration-300 {isVisible
? 'translate-y-0'
: '-translate-y-full'}"
style="top: {navbarTop}px;"
>
<div class="mx-auto flex space-x-2 container">
<div class="flex items-center space-x-2 md:min-w-52 min-w-8">
{#if shouldShowBack()}
<Button
class="btn-leather !w-auto sm:hidden"
outline={true}
onclick={backToMain}
>
<CaretLeftOutline class="!fill-none inline mr-1" />
<span class="hidden sm:inline">Back</span>
</Button>
{/if}
{#if isIndexEvent}
{#if publicationType === "blog"}
<Button
class={`btn-leather hidden sm:flex !w-auto ${$publicationColumnVisibility.blog ? "active" : ""}`}
outline={true}
onclick={() => toggleColumn("blog")}
>
<BookOutline class="!fill-none inline mr-1" />
<span class="hidden sm:inline">Table of Contents</span>
</Button>
{:else if !$publicationColumnVisibility.discussion && !$publicationColumnVisibility.toc}
<!-- Blog view: hidden when showing blog list, shows back arrow when viewing article -->
{#if !($publicationColumnVisibility.blog && !$publicationColumnVisibility.inner)}
<Button
class={`btn-leather !w-auto ${$publicationColumnVisibility.inner ? "active" : ""}`}
outline={true}
onclick={handleBlogTocClick}
title={$publicationColumnVisibility.inner ? "Back to Table of Contents" : "Table of Contents"}
>
{#if $publicationColumnVisibility.inner}
<CaretLeftOutline class="!fill-none" />
{:else}
<BookOutline class="!fill-none" />
{/if}
</Button>
{/if}
{:else if !$publicationColumnVisibility.discussion}
<Button
class={`btn-leather !w-auto ${$publicationColumnVisibility.toc ? "active" : ""}`}
outline={true}
onclick={() => toggleColumn("toc")}
title="Table of Contents"
>
<BookOutline class="!fill-none inline mr-1" />
<span class="hidden sm:inline">Table of Contents</span>
<BookOutline class="!fill-none" />
</Button>
{/if}
{/if}

18
src/lib/components/util/CardActions.svelte

@ -26,10 +26,16 @@ @@ -26,10 +26,16 @@
import { WebSocketPool } from "$lib/data_structures/websocket_pool";
// Component props
let { event, onDelete, sectionAddress } = $props<{
let {
event,
onDelete,
sectionAddress,
detailsModalOpen = $bindable(false)
} = $props<{
event: NDKEvent;
onDelete?: () => void;
sectionAddress?: string; // If provided, shows "Comment on section" option
detailsModalOpen?: boolean; // Bindable prop to control modal from outside
}>();
const ndk = getNdkContext();
@ -72,8 +78,7 @@ @@ -72,8 +78,7 @@
event.tags.find((t: string[]) => t[0] === "identifier")?.[1] ?? null,
);
// UI state
let detailsModalOpen: boolean = $state(false);
// UI state - detailsModalOpen is now a bindable prop
let isOpen: boolean = $state(false);
// Comment modal state
@ -408,7 +413,7 @@ @@ -408,7 +413,7 @@
</script>
<div
class="group bg-highlight dark:bg-primary-1000 rounded"
class="group bg-transparent rounded"
role="group"
onmouseenter={openPopover}
>
@ -416,8 +421,7 @@ @@ -416,8 +421,7 @@
<Button
type="button"
id="dots-{event.id}"
class=" hover:bg-primary-50 dark:text-highlight dark:hover:bg-primary-800 p-1 dots"
color="primary"
class="!bg-transparent hover:!bg-primary-100 dark:hover:!bg-primary-800 text-primary-600 dark:text-gray-300 hover:text-primary-700 dark:hover:text-gray-200 p-1 dots !border-0 !shadow-none"
data-popover-target="popover-actions"
>
<DotsVerticalOutline class="h-6 w-6" />
@ -536,7 +540,7 @@ @@ -536,7 +540,7 @@
<div class="flex flex-row">
<h4 class="text-base font-normal mt-2">
Index author: {@render userBadge(event.pubkey, author, ndk)}
{event.kind === 30040 ? "Index author" : "Article author"}: {@render userBadge(event.pubkey, author, ndk)}
</h4>
</div>

14
src/lib/components/util/Details.svelte

@ -14,10 +14,11 @@ @@ -14,10 +14,11 @@
// isModal
// - don't show interactions in modal view
// - don't show all the details when _not_ in modal view
let { event, isModal = false, onDelete } = $props<{
let { event, isModal = false, onDelete, hideActions = false } = $props<{
event: any;
isModal?: boolean;
onDelete?: () => void;
hideActions?: boolean;
}>();
let title: string = $derived(getMatchingTags(event, "title")[0]?.[1]);
@ -66,7 +67,7 @@ @@ -66,7 +67,7 @@
</script>
<div class="flex flex-col relative mb-2">
{#if !isModal}
{#if !isModal && !hideActions}
<div class="flex flex-row justify-between items-center">
<!-- Index author badge -->
<P class="text-base font-normal"
@ -76,6 +77,13 @@ @@ -76,6 +77,13 @@
<CardActions {event} {onDelete}></CardActions>
</div>
</div>
{:else if !isModal && hideActions}
<div class="flex flex-row justify-between items-center">
<!-- Index author badge -->
<P class="text-base font-normal"
>{@render userBadge(event.pubkey, undefined, ndk)}</P
>
</div>
{/if}
<div
class="flex-grow grid grid-cols-1 md:grid-cols-[auto_1fr] gap-4 items-center"
@ -134,7 +142,7 @@ @@ -134,7 +142,7 @@
{#each hashtags as tag}
<button
onclick={() => goto(`/events?t=${encodeURIComponent(tag)}`)}
class="text-sm hover:text-primary-700 dark:hover:text-primary-300 cursor-pointer mr-2"
class="text-sm text-primary-700 dark:text-primary-300 hover:text-primary-800 dark:hover:text-primary-200 cursor-pointer mr-2"
>#{tag}</button
>
{/each}

36
src/lib/data_structures/websocket_pool.ts

@ -178,19 +178,39 @@ export class WebSocketPool { @@ -178,19 +178,39 @@ export class WebSocketPool {
* URL, the connection is passed to the requestor in the queue. Otherwise, the connection is
* marked as available.
*
* @param handle - The WebSocketHandle to release.
* This method is defensive: if the connection is no longer in the pool (e.g., it was already
* closed and removed), it returns silently rather than throwing an error.
*
* @param ws - The WebSocket connection to release.
*/
public release(ws: WebSocket): void {
const normalizedUrl = this.#normalizeUrl(ws.url);
const handle = this.#pool.get(normalizedUrl);
if (!handle) {
throw new Error(
"[WebSocketPool] Attempted to release an unmanaged WebSocket connection.",
// AI-NOTE: Defensive check - if WebSocket is closed or doesn't have a URL, skip release
if (!ws || !ws.url) {
console.warn(
"[WebSocketPool] Attempted to release an invalid WebSocket connection (no URL).",
);
return;
}
if (--handle.refCount === 0) {
this.#startIdleTimer(handle);
try {
const normalizedUrl = this.#normalizeUrl(ws.url);
const handle = this.#pool.get(normalizedUrl);
if (!handle) {
// AI-NOTE: Connection may have been removed due to closure or error - this is acceptable
console.debug(
`[WebSocketPool] Connection to ${normalizedUrl} is no longer in pool (likely already closed).`,
);
return;
}
if (--handle.refCount === 0) {
this.#startIdleTimer(handle);
}
} catch (error) {
// AI-NOTE: If URL normalization fails or other errors occur, log but don't throw
console.warn(
`[WebSocketPool] Error releasing connection: ${error}. This may occur if the connection was already closed.`,
);
}
}

9
src/lib/services/publisher.ts

@ -134,10 +134,13 @@ export async function publishSingleEvent( @@ -134,10 +134,13 @@ export async function publishSingleEvent(
dTag: tags.find((t) => t[0] === "d")?.[1],
});
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 300 + Math.random() * 200));
await new Promise((resolve) =>
setTimeout(resolve, 300 + Math.random() * 200)
);
// Generate a fake event ID
const fakeEventId = Array.from({ length: 64 }, () =>
Math.floor(Math.random() * 16).toString(16),
const fakeEventId = Array.from(
{ length: 64 },
() => Math.floor(Math.random() * 16).toString(16),
).join("");
return { success: true, eventId: fakeEventId };
}

2
src/lib/snippets/PublicationSnippets.svelte

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
{@const headingLevel = Math.min(depth + 1, 6)}
<!-- TODO: Handle floating titles. -->
<svelte:element this={`h${headingLevel}`} class="h-leather">
<svelte:element this={`h${headingLevel}`} class="h-leather text-left">
{title}
</svelte:element>
{/snippet}

13
src/lib/stores/themeStore.ts

@ -10,8 +10,17 @@ export const theme = writable(initial); @@ -10,8 +10,17 @@ export const theme = writable(initial);
theme.subscribe((v) => {
if (typeof document !== "undefined") {
document.documentElement.dataset.theme = String(v);
localStorage.setItem(KEY, String(v));
const themeValue = String(v);
document.documentElement.dataset.theme = themeValue;
localStorage.setItem(KEY, themeValue);
// Add .dark class for non-light themes (ocean, forrest are dark themes)
// Remove .dark class for light theme
if (themeValue === "light") {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
}
});

11
src/routes/+layout.svelte

@ -13,6 +13,8 @@ @@ -13,6 +13,8 @@
setContext("ndk", data.ndk);
let contentTop = $state(100); // Default to 100px
// Get standard metadata for OpenGraph tags
let title = "Library of Alexandria";
let currentUrl = page.url.href;
@ -27,6 +29,13 @@ @@ -27,6 +29,13 @@
const rect = document.body.getBoundingClientRect();
// document.body.style.height = `${rect.height}px`;
// Measure the actual navbar height to position content correctly
const navbar = document.getElementById("navi");
if (navbar) {
const navbarRect = navbar.getBoundingClientRect();
contentTop = navbarRect.bottom;
}
// AI-NOTE: Restore authentication state from localStorage on page load
// This function automatically restores the user's login state when the page is refreshed,
// preventing the user from being logged out unexpectedly. It handles extension, npub, and Amber logins.
@ -183,7 +192,7 @@ @@ -183,7 +192,7 @@
<div class="min-h-screen flex flex-col">
<ANavbar />
<div class="flex flex-1 flex-col w-full mt-[100px] self-center">
<div class="flex flex-1 flex-col w-full self-center" style="margin-top: {contentTop}px;">
{@render children()}
</div>

2
src/routes/publication/[type]/[identifier]/+page.svelte

@ -90,8 +90,6 @@ @@ -90,8 +90,6 @@
function initializePublicationComponents(event: NDKEvent) {
if (!data.ndk) return;
console.log("[Publication] Initializing publication components for event:", event.tagAddress());
publicationTree = new SveltePublicationTree(event, data.ndk);
toc = new TableOfContents(
event.tagAddress(),

28
src/styles/a/cards.css

@ -18,12 +18,12 @@ @@ -18,12 +18,12 @@
.card-leather h5,
.card-leather h6 {
@apply text-gray-900 hover:text-primary-600 dark:text-gray-100
dark:hover:text-primary-400;
dark:hover:text-primary-300;
}
.card-leather .font-thin {
@apply text-gray-900 hover:text-primary-600 dark:text-gray-100
dark:hover:text-primary-400;
@apply text-gray-900 hover:text-primary-700 dark:text-gray-100
dark:hover:text-primary-300;
}
/* Main card leather (used in profile previews) */
@ -61,12 +61,12 @@ @@ -61,12 +61,12 @@
.ArticleBox h5,
.ArticleBox h6 {
@apply text-gray-900 hover:text-primary-600 dark:text-gray-100
dark:hover:text-primary-400;
dark:hover:text-primary-300;
}
.ArticleBox .font-thin {
@apply text-gray-900 hover:text-primary-600 dark:text-gray-100
dark:hover:text-primary-400;
@apply text-gray-900 hover:text-primary-700 dark:text-gray-100
dark:hover:text-primary-300;
}
/* Article box image transitions */
@ -155,24 +155,24 @@ @@ -155,24 +155,24 @@
flex items-center gap-2 flex-wrap;
}
/* Card content text styles */
/* Card content text styles - improved contrast */
.card-summary {
@apply text-sm text-primary-900 dark:text-primary-200 line-clamp-2;
@apply text-sm text-primary-900 dark:text-primary-100 line-clamp-2;
}
.card-content {
@apply text-sm text-gray-800 dark:text-gray-200 line-clamp-3 break-words
@apply text-sm text-gray-900 dark:text-gray-100 line-clamp-3 break-words
mb-4;
}
.card-about {
@apply text-sm text-gray-700 dark:text-gray-300 line-clamp-3;
@apply text-sm text-gray-800 dark:text-gray-200 line-clamp-3;
}
/* Deferral link styling */
/* Deferral link styling - improved contrast */
.deferral-link {
@apply underline text-primary-700 dark:text-primary-400
hover:text-primary-600 dark:hover:text-primary-400 break-all
@apply underline text-primary-700 dark:text-primary-300
hover:text-primary-800 dark:hover:text-primary-200 break-all
cursor-pointer;
}
@ -182,7 +182,7 @@ @@ -182,7 +182,7 @@
.tags span {
@apply bg-primary-50 text-primary-800 text-sm font-medium me-2 px-2.5 py-0.5
rounded-sm dark:bg-primary-900 dark:text-primary-200;
rounded-sm dark:bg-primary-900 dark:text-primary-100;
}
/* ========================================

2
src/styles/base.css

@ -2,6 +2,6 @@ @@ -2,6 +2,6 @@
@layer components {
body {
@apply bg-primary-0 dark:bg-primary-1000;
@apply bg-primary-0 dark:bg-primary-1000 text-gray-900 dark:text-gray-100;
}
}

12
src/styles/notifications.css

@ -50,15 +50,15 @@ @@ -50,15 +50,15 @@
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
}
/* Filter button states */
/* Filter button states - improved contrast */
.filter-button-active {
background-color: rgb(107 114 128);
color: rgb(243 244 246);
background-color: rgb(75 85 99);
color: rgb(255 255 255);
}
.dark .filter-button-active {
background-color: rgb(107 114 128);
color: rgb(243 244 246);
color: rgb(255 255 255);
}
/* Reply button hover states */
@ -181,11 +181,11 @@ @@ -181,11 +181,11 @@
}
.mode-toggle-button.inactive {
color: rgb(55 65 81);
color: rgb(31 41 55);
}
.dark .mode-toggle-button.inactive {
color: rgb(156 163 175);
color: rgb(209 213 219);
}
.mode-toggle-button.inactive:hover {

2
src/styles/publications.css

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
}
.publication-leather p a {
@apply underline hover:text-primary-600 dark:hover:text-primary-400;
@apply underline hover:text-primary-700 dark:hover:text-primary-300;
}
.publication-leather section p {

20
src/styles/visualize.css

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
@layer components {
/* Legend styles - specific to visualization */
/* Legend styles - specific to visualization - improved contrast */
.legend-list {
@apply list-disc mt-2 space-y-2 text-gray-800 dark:text-gray-300;
@apply list-disc mt-2 space-y-2 text-gray-900 dark:text-gray-200;
}
.legend-item {
@ -70,7 +70,7 @@ @@ -70,7 +70,7 @@
}
.network-debug {
@apply mt-4 text-sm text-gray-500;
@apply mt-4 text-sm text-gray-700 dark:text-gray-300;
}
/* Zoom controls */
@ -82,11 +82,11 @@ @@ -82,11 +82,11 @@
@apply bg-white;
}
/* Tooltip styles - specific to visualization tooltips */
/* Tooltip styles - specific to visualization tooltips - improved contrast */
.tooltip-close-btn {
@apply absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700
dark:hover:bg-gray-600 rounded-full p-1 text-gray-500 hover:text-gray-700
dark:text-gray-400 dark:hover:text-gray-200;
dark:hover:bg-gray-600 rounded-full p-1 text-gray-700 hover:text-gray-900
dark:text-gray-300 dark:hover:text-gray-100;
}
.tooltip-content {
@ -98,12 +98,12 @@ @@ -98,12 +98,12 @@
}
.tooltip-title-link {
@apply text-gray-800 hover:text-blue-600 dark:text-gray-200
dark:hover:text-blue-400;
@apply text-gray-900 hover:text-blue-700 dark:text-gray-100
dark:hover:text-blue-300;
}
.tooltip-metadata {
@apply text-gray-600 dark:text-gray-400 text-sm;
@apply text-gray-700 dark:text-gray-300 text-sm;
}
.tooltip-summary {
@ -117,7 +117,7 @@ @@ -117,7 +117,7 @@
}
.tooltip-help-text {
@apply mt-2 text-xs text-gray-500 dark:text-gray-400 italic;
@apply mt-2 text-xs text-gray-600 dark:text-gray-300 italic;
}
/* Star network visualization styles */

11
src/theme-tokens.css

@ -15,6 +15,13 @@ @@ -15,6 +15,13 @@
--brand-primary-1000: #15110d;
}
/* Dark mode tweaks for default theme - improve contrast for text on dark backgrounds */
:root.dark {
/* Brighten mid-tones for better contrast when used as text */
--brand-primary-300: #d4a574;
--brand-primary-200: #e0b890;
}
/* Example alternative theme: ocean */
:root[data-theme="ocean"] {
--brand-primary-0: #ecf8ff;
@ -37,6 +44,8 @@ @@ -37,6 +44,8 @@
/* nudge the mid tones brighter for contrast */
--brand-primary-400: #7ccdfc;
--brand-primary-500: #38bdf8;
--brand-primary-300: #a5d8f5;
--brand-primary-200: #bae6fd;
}
/* Example alternative theme: forrest */
@ -61,4 +70,6 @@ @@ -61,4 +70,6 @@
/* nudge the mid tones brighter for contrast */
--brand-primary-400: #7fc97f;
--brand-primary-500: #4caf50;
--brand-primary-300: #9fda9f;
--brand-primary-200: #b8e6b8;
}

Loading…
Cancel
Save