You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
668 lines
24 KiB
668 lines
24 KiB
<script lang="ts"> |
|
import FeedPost from '../feed/FeedPost.svelte'; |
|
import CommentThread from '../comments/CommentThread.svelte'; |
|
import { nostrClient } from '../../services/nostr/nostr-client.js'; |
|
import { relayManager } from '../../services/nostr/relay-manager.js'; |
|
import { loadEventIndex, type EventIndexItem, type MissingEventInfo } from '../../services/nostr/event-index-loader.js'; |
|
import { onMount } from 'svelte'; |
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
import { KIND } from '../../types/kind-lookup.js'; |
|
import MetadataCard from '../../components/content/MetadataCard.svelte'; |
|
|
|
interface Props { |
|
eventId: string; |
|
preloadedEvent?: NostrEvent | null; // Optional preloaded event to avoid re-fetching |
|
} |
|
|
|
let { eventId, preloadedEvent = null }: Props = $props(); |
|
|
|
// Virtual scrolling for kind 30040 (event indexes with 36k+ events) |
|
let Virtualizer: any = $state(null); |
|
let virtualizerLoading = $state(false); |
|
let virtualizerContainer = $state<HTMLElement | null>(null); |
|
|
|
async function loadVirtualizer() { |
|
if (Virtualizer) return Virtualizer; |
|
if (virtualizerLoading) return null; |
|
|
|
virtualizerLoading = true; |
|
try { |
|
const module = await import('@tanstack/svelte-virtual'); |
|
Virtualizer = module.Virtualizer; |
|
return Virtualizer; |
|
} catch (error) { |
|
console.error('Failed to load virtual scrolling:', error); |
|
return null; |
|
} finally { |
|
virtualizerLoading = false; |
|
} |
|
} |
|
|
|
let rootEvent = $state<NostrEvent | null>(null); |
|
let loading = $state(true); |
|
let error = $state<string | null>(null); |
|
let lastLoadedEventId = $state<string | null>(null); // Track last loaded eventId to prevent loops |
|
|
|
// For kind 30040: event index hierarchy |
|
let isEventIndex = $derived(rootEvent?.kind === 30040); |
|
let eventIndexItems = $state<EventIndexItem[]>([]); |
|
let missingEvents = $state<MissingEventInfo[]>([]); |
|
let loadingIndex = $state(false); |
|
let indexError = $state<string | null>(null); |
|
|
|
// Count total items including nested ones for virtual scrolling decision |
|
function countTotalItems(items: EventIndexItem[]): number { |
|
let count = 0; |
|
for (const item of items) { |
|
count += 1; |
|
if (item.children && item.children.length > 0) { |
|
count += countTotalItems(item.children); |
|
} |
|
} |
|
return count; |
|
} |
|
|
|
let totalItemCount = $derived(countTotalItems(eventIndexItems)); |
|
|
|
// Check if event is metadata-only (no content to display) |
|
// These kinds should only show metadata, not content |
|
const METADATA_ONLY_KINDS: number[] = [ |
|
KIND.METADATA, // 0 - Profile metadata |
|
KIND.CONTACTS, // 3 - Contact list |
|
KIND.RELAY_LIST, // 10002 - Relay list metadata |
|
KIND.MUTE_LIST, // 10000 - Mute list |
|
KIND.BLOCKED_RELAYS, // 10006 - Blocked relays |
|
KIND.FAVORITE_RELAYS, // 10012 - Favorite relays |
|
KIND.LOCAL_RELAYS, // 10432 - Local relays |
|
KIND.PIN_LIST, // 10001 - Pin list |
|
KIND.BOOKMARKS, // 10003 - Bookmarks |
|
KIND.INTEREST_LIST, // 10015 - Interest list |
|
KIND.EMOJI_SET, // 10030 - Emoji set |
|
KIND.EMOJI_PACK, // 30030 - Emoji pack |
|
KIND.BADGES, // 30008 - Badges |
|
KIND.FOLLOW_SET, // 30000 - Follow set |
|
KIND.PAYMENT_ADDRESSES, // 10133 - Payment addresses |
|
KIND.LABEL, // 1985 - Label |
|
KIND.REPORT, // 1984 - Report |
|
KIND.HTTP_AUTH, // 27235 - HTTP Auth |
|
KIND.RSS_FEED, // 10895 - RSS Feed |
|
KIND.REPO_ANNOUNCEMENT, // 30617 - Repository announcement (metadata only) |
|
KIND.USER_STATUS, // 30315 - User status (metadata only, shown in profile) |
|
]; |
|
|
|
// Kinds that should render even with empty content (they have media or special rendering) |
|
const ALWAYS_RENDER_CONTENT_KINDS: number[] = [ |
|
KIND.PICTURE_NOTE, // 20 - Has images in imeta tags |
|
KIND.VIDEO_NOTE, // 21 - Has videos in imeta tags |
|
KIND.SHORT_VIDEO_NOTE, // 22 - Has videos in imeta tags |
|
KIND.VOICE_NOTE, // 1222 - Has audio in imeta tags |
|
KIND.VOICE_REPLY, // 1244 - Has audio in imeta tags |
|
KIND.FILE_METADATA, // 1063 - Has files in tags |
|
KIND.POLL, // 1068 - Has poll rendering |
|
KIND.PUBLICATION_INDEX, // 30040 - Has children to render |
|
KIND.ISSUE, // 1621 - Repository issues (may have empty content but have tags) |
|
KIND.STATUS_OPEN, // 1630 - Status events |
|
KIND.STATUS_APPLIED, // 1631 |
|
KIND.STATUS_CLOSED, // 1632 |
|
KIND.STATUS_DRAFT, // 1633 |
|
]; |
|
|
|
let isMetadataOnly = $derived(rootEvent ? ( |
|
METADATA_ONLY_KINDS.includes(rootEvent.kind) || |
|
(rootEvent.content === '' && |
|
rootEvent.tags.length > 0 && |
|
!ALWAYS_RENDER_CONTENT_KINDS.includes(rootEvent.kind)) |
|
) : false); |
|
|
|
onMount(async () => { |
|
await nostrClient.initialize(); |
|
loadEvent(); |
|
}); |
|
|
|
$effect(() => { |
|
// Only load if eventId changed and we're not already loading |
|
// Also reload if preloadedEvent changes (e.g., from null to an actual event) |
|
if (eventId && eventId !== lastLoadedEventId && !loading) { |
|
loadEvent(); |
|
} else if (preloadedEvent && preloadedEvent.id === eventId && !rootEvent && !loading) { |
|
// If we have a preloaded event and haven't loaded yet, use it immediately |
|
rootEvent = preloadedEvent; |
|
loading = false; |
|
if (preloadedEvent.kind === 30040) { |
|
loadEventIndexHierarchy(preloadedEvent); |
|
loadVirtualizer(); |
|
} |
|
} |
|
}); |
|
|
|
async function loadEvent() { |
|
// Prevent loading if already loading the same event |
|
if (loading && lastLoadedEventId === eventId) { |
|
return; |
|
} |
|
|
|
loading = true; |
|
error = null; |
|
indexError = null; |
|
eventIndexItems = []; |
|
rootEvent = null; |
|
lastLoadedEventId = eventId; // Track that we're loading this eventId |
|
|
|
try { |
|
// Use preloaded event if available and matches eventId |
|
let event: NostrEvent | null = null; |
|
if (preloadedEvent && preloadedEvent.id === eventId) { |
|
event = preloadedEvent; |
|
} else { |
|
// Load the event by ID |
|
const threadRelays = relayManager.getThreadReadRelays(); |
|
const feedRelays = relayManager.getFeedReadRelays(); |
|
const allRelays = [...new Set([...threadRelays, ...feedRelays])]; |
|
event = await nostrClient.getEventById(eventId, allRelays); |
|
} |
|
|
|
if (event) { |
|
rootEvent = event; |
|
|
|
// If it's a kind 30040 event index, load the entire hierarchy |
|
if (event.kind === 30040) { |
|
await loadEventIndexHierarchy(event); |
|
// Load virtualizer for large lists |
|
await loadVirtualizer(); |
|
} |
|
} else { |
|
error = 'Event not found'; |
|
} |
|
} catch (err) { |
|
console.error('Error loading event:', err); |
|
error = err instanceof Error ? err.message : 'Failed to load event'; |
|
} finally { |
|
loading = false; |
|
} |
|
} |
|
|
|
async function loadEventIndexHierarchy(opEvent: NostrEvent) { |
|
if (opEvent.kind !== 30040) return; |
|
|
|
loadingIndex = true; |
|
indexError = null; |
|
missingEvents = []; |
|
try { |
|
// console.log('Loading event index hierarchy for kind 30040...'); |
|
const result = await loadEventIndex(opEvent); |
|
eventIndexItems = result.items; |
|
missingEvents = result.missingEvents; |
|
// console.log(`Loaded ${result.items.length} events from index hierarchy`); |
|
// Debug: log items with children |
|
// const itemsWithChildren = result.items.filter(item => item.children && item.children.length > 0); |
|
// console.log(`[EventView] Items with children: ${itemsWithChildren.length}`, itemsWithChildren.map(item => ({ |
|
// id: item.event.id, |
|
// kind: item.event.kind, |
|
// level: item.level, |
|
// childrenCount: item.children?.length || 0, |
|
// title: item.event.tags.find(t => t[0] === 'title')?.[1] |
|
// }))); |
|
if (result.missingEvents.length > 0) { |
|
console.warn(`[EventView] ${result.missingEvents.length} events are missing from the index hierarchy`); |
|
} |
|
} catch (err) { |
|
console.error('Error loading event index:', err); |
|
indexError = err instanceof Error ? err.message : 'Failed to load event index'; |
|
} finally { |
|
loadingIndex = false; |
|
} |
|
} |
|
</script> |
|
|
|
{#if loading} |
|
<p class="text-fog-text dark:text-fog-dark-text">Loading event...</p> |
|
{:else if error} |
|
<p class="text-fog-text dark:text-fog-dark-text error-message">Error: {error}</p> |
|
{:else if rootEvent} |
|
<article class="event-view"> |
|
<!-- Display title and metadata for kind 30040 (book index) --> |
|
{#if rootEvent.kind === 30040} |
|
{@const titleTag = rootEvent.tags.find(t => t[0] === 'title')} |
|
{@const authorTag = rootEvent.tags.find(t => t[0] === 'author')} |
|
{@const typeTag = rootEvent.tags.find(t => t[0] === 'type')} |
|
{@const versionTag = rootEvent.tags.find(t => t[0] === 'version')} |
|
{@const publishedOnTag = rootEvent.tags.find(t => t[0] === 'published_on')} |
|
{@const publishedByTag = rootEvent.tags.find(t => t[0] === 'published_by')} |
|
{@const imageTag = rootEvent.tags.find(t => t[0] === 'image')} |
|
{@const summaryTag = rootEvent.tags.find(t => t[0] === 'summary')} |
|
{@const sourceTag = rootEvent.tags.find(t => t[0] === 'source')} |
|
{@const iTags = rootEvent.tags.filter(t => t[0] === 'i')} |
|
{@const tTags = rootEvent.tags.filter(t => t[0] === 't')} |
|
{@const pTag = rootEvent.tags.find(t => t[0] === 'p')} |
|
{@const ETag = rootEvent.tags.find(t => t[0] === 'E')} |
|
|
|
{#if titleTag && titleTag[1]} |
|
<h1 class="book-title text-3xl font-bold mb-6 text-fog-text dark:text-fog-dark-text"> |
|
{titleTag[1]} |
|
</h1> |
|
{/if} |
|
|
|
<!-- Publication metadata --> |
|
<div class="publication-metadata mb-6 space-y-2"> |
|
{#if authorTag && authorTag[1]} |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
<span class="font-semibold">Author:</span> {authorTag[1]} |
|
</p> |
|
{/if} |
|
|
|
{#if typeTag && typeTag[1]} |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
<span class="font-semibold">Type:</span> {typeTag[1]} |
|
</p> |
|
{/if} |
|
|
|
{#if versionTag && versionTag[1]} |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
<span class="font-semibold">Version:</span> {versionTag[1]} |
|
</p> |
|
{/if} |
|
|
|
{#if publishedOnTag && publishedOnTag[1]} |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
<span class="font-semibold">Published:</span> {publishedOnTag[1]} |
|
{#if publishedByTag && publishedByTag[1]} |
|
<span> by {publishedByTag[1]}</span> |
|
{/if} |
|
</p> |
|
{:else if publishedByTag && publishedByTag[1]} |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
<span class="font-semibold">Published by:</span> {publishedByTag[1]} |
|
</p> |
|
{/if} |
|
|
|
{#if iTags.length > 0} |
|
{@const plural = iTags.length > 1 ? 's' : ''} |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
<span class="font-semibold">Identifier{plural}:</span> |
|
{#each iTags as iTag, index} |
|
{iTag[1]}{#if index < iTags.length - 1}, {/if} |
|
{/each} |
|
</p> |
|
{/if} |
|
|
|
{#if tTags.length > 0} |
|
{@const plural = tTags.length > 1 ? 's' : ''} |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
<span class="font-semibold">Topic{plural}:</span> |
|
{#each tTags as tTag} |
|
<span class="topic-tag">{tTag[1]}</span> |
|
{/each} |
|
</p> |
|
{/if} |
|
|
|
{#if sourceTag && sourceTag[1]} |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
<span class="font-semibold">Source:</span> |
|
<a href={sourceTag[1]} target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline"> |
|
{sourceTag[1]} |
|
</a> |
|
</p> |
|
{/if} |
|
|
|
{#if summaryTag && summaryTag[1]} |
|
<p class="text-fog-text dark:text-fog-dark-text mt-4"> |
|
{summaryTag[1]} |
|
</p> |
|
{/if} |
|
|
|
{#if imageTag && imageTag[1]} |
|
<div class="publication-cover-image mt-4"> |
|
<img src={imageTag[1]} alt="Cover image for {titleTag?.[1] || 'publication'}" class="max-w-md rounded-lg shadow-lg" /> |
|
</div> |
|
{/if} |
|
|
|
{#if pTag && pTag[1]} |
|
<p class="text-fog-text dark:text-fog-dark-text mt-4 text-sm italic"> |
|
<span class="font-semibold">Original author:</span> |
|
<a href="/profile/{pTag[1]}" class="text-blue-600 dark:text-blue-400 hover:underline"> |
|
{pTag[1]} |
|
</a> |
|
{#if ETag && ETag[1]} |
|
<span> (derivative work)</span> |
|
{/if} |
|
</p> |
|
{/if} |
|
</div> |
|
{/if} |
|
|
|
<!-- Display metadata-only events with MetadataCard --> |
|
{#if isMetadataOnly} |
|
<div class="metadata-section"> |
|
<MetadataCard event={rootEvent} /> |
|
</div> |
|
{:else if isEventIndex} |
|
<!-- For kind 30040: Display event index hierarchy with virtual scrolling --> |
|
<div class="event-index-section"> |
|
{#if loadingIndex} |
|
<p class="text-fog-text dark:text-fog-dark-text">Loading event index hierarchy... ({eventIndexItems.length} loaded so far)</p> |
|
{:else if indexError} |
|
<p class="text-fog-text dark:text-fog-dark-text error-message">Error: {indexError}</p> |
|
{:else if eventIndexItems.length === 0 && missingEvents.length === 0} |
|
<p class="text-fog-text dark:text-fog-dark-text">No events found in index</p> |
|
{:else} |
|
<!-- Display missing events first --> |
|
{#if missingEvents.length > 0} |
|
<div class="missing-events-section mb-4"> |
|
{#each missingEvents as missing (missing.order)} |
|
<div class="missing-event-message"> |
|
<p class="text-fog-text dark:text-fog-dark-text"> |
|
This event <code class="d-tag-code">{missing.dTag}</code> is missing. |
|
</p> |
|
</div> |
|
{/each} |
|
</div> |
|
{/if} |
|
|
|
{#if eventIndexItems.length > 0} |
|
<!-- Recursive rendering function for nested structures --> |
|
{#snippet renderIndexItems(items: EventIndexItem[], parentLevel: number = 0)} |
|
{#each items as item (item.event.id)} |
|
<div class="event-index-item" data-index={item.order} data-level={item.level}> |
|
<!-- Display title for kind 30040 indexes (chapter/subchapter indexes) --> |
|
{#if item.event.kind === 30040} |
|
{@const indexTitleTag = item.event.tags.find(t => t[0] === 'title')} |
|
{#if indexTitleTag && indexTitleTag[1]} |
|
{@const headingLevel = Math.min(2 + item.level, 6)} |
|
{@const textSize = item.level === 0 ? 'xl' : item.level === 1 ? 'lg' : 'base'} |
|
{#if headingLevel === 2} |
|
<h2 class="index-title text-{textSize} font-semibold mb-3 mt-4 text-fog-text dark:text-fog-dark-text"> |
|
{indexTitleTag[1]} |
|
</h2> |
|
{:else if headingLevel === 3} |
|
<h3 class="index-title text-{textSize} font-semibold mb-3 mt-4 text-fog-text dark:text-fog-dark-text"> |
|
{indexTitleTag[1]} |
|
</h3> |
|
{:else if headingLevel === 4} |
|
<h4 class="index-title text-{textSize} font-semibold mb-3 mt-4 text-fog-text dark:text-fog-dark-text"> |
|
{indexTitleTag[1]} |
|
</h4> |
|
{:else if headingLevel === 5} |
|
<h5 class="index-title text-{textSize} font-semibold mb-3 mt-4 text-fog-text dark:text-fog-dark-text"> |
|
{indexTitleTag[1]} |
|
</h5> |
|
{:else} |
|
<h6 class="index-title text-{textSize} font-semibold mb-3 mt-4 text-fog-text dark:text-fog-dark-text"> |
|
{indexTitleTag[1]} |
|
</h6> |
|
{/if} |
|
{/if} |
|
|
|
<!-- Recursively render children of kind 30040 indexes --> |
|
{#if item.children && item.children.length > 0} |
|
<div class="nested-index" style="margin-left: {item.level * 1.5}rem; margin-top: 1rem;"> |
|
{@render renderIndexItems(item.children, item.level)} |
|
</div> |
|
{/if} |
|
{:else} |
|
<!-- For content events (not kind 30040 indexes), render the content --> |
|
<!-- Display chapter title prominently for kind 30041 (chapter sections) --> |
|
{#if item.event.kind === 30041 || item.event.kind === 1 || item.event.kind === 30817} |
|
{@const chapterTitleTag = item.event.tags.find(t => t[0] === 'title')} |
|
{#if chapterTitleTag && chapterTitleTag[1]} |
|
<h2 class="chapter-title text-xl font-semibold mb-3 text-fog-text dark:text-fog-dark-text"> |
|
{chapterTitleTag[1]} |
|
</h2> |
|
{/if} |
|
{/if} |
|
|
|
<!-- Render the event content --> |
|
<div class="event-with-comments"> |
|
<!-- Hide title in FeedPost since we're already showing it above as chapter-title --> |
|
<FeedPost post={item.event} fullView={true} hideTitle={true} /> |
|
|
|
<!-- Load and display comments for each event in the index --> |
|
<div class="comments-section mt-4"> |
|
<CommentThread threadId={item.event.id} event={item.event} /> |
|
</div> |
|
</div> |
|
|
|
<!-- Recursively render children if this content event has nested children --> |
|
{#if item.children && item.children.length > 0} |
|
<div class="nested-index" style="margin-left: {item.level * 1.5}rem; margin-top: 1rem;"> |
|
{@render renderIndexItems(item.children, item.level)} |
|
</div> |
|
{/if} |
|
{/if} |
|
</div> |
|
{/each} |
|
{/snippet} |
|
|
|
<!-- Note: Virtual scrolling doesn't work well with nested structures, so we disable it for nested indexes --> |
|
{#if Virtualizer && totalItemCount > 100 && eventIndexItems.every(item => !item.children || item.children.length === 0)} |
|
<!-- Use virtual scrolling only for flat lists (100+ items, no nesting) --> |
|
{#if Virtualizer} |
|
{@const V = Virtualizer} |
|
<div bind:this={virtualizerContainer} class="virtual-scroll-container" style="height: 80vh; overflow: auto;"> |
|
<V |
|
count={eventIndexItems.length} |
|
getScrollElement={() => virtualizerContainer} |
|
estimateSize={() => 200} |
|
overscan={5} |
|
> |
|
{@render renderIndexItems(eventIndexItems)} |
|
</V> |
|
</div> |
|
{/if} |
|
{:else} |
|
<!-- Progressive rendering for smaller lists or nested structures --> |
|
<div class="event-index-list"> |
|
{@render renderIndexItems(eventIndexItems)} |
|
</div> |
|
{/if} |
|
{/if} |
|
{/if} |
|
</div> |
|
{:else} |
|
<!-- Display regular events using FeedPost --> |
|
<div class="event-section"> |
|
<FeedPost post={rootEvent} fullView={true} hideTitle={false} /> |
|
</div> |
|
|
|
<!-- Load and display comments for all event types --> |
|
{#if rootEvent && !isMetadataOnly} |
|
<div class="comments-section mt-6"> |
|
<CommentThread threadId={rootEvent.id} event={rootEvent} /> |
|
</div> |
|
{/if} |
|
{/if} |
|
</article> |
|
{:else} |
|
<p class="text-fog-text dark:text-fog-dark-text">Event not found</p> |
|
{/if} |
|
|
|
<style> |
|
.event-view { |
|
max-width: var(--content-width); |
|
margin: 0 auto; |
|
padding: 1rem; |
|
} |
|
|
|
.metadata-section { |
|
margin-bottom: 2rem; |
|
} |
|
|
|
.event-section { |
|
margin-bottom: 2rem; |
|
} |
|
|
|
.event-index-section { |
|
margin-top: 2rem; |
|
} |
|
|
|
.virtual-scroll-container { |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.5rem; |
|
padding: 1rem; |
|
} |
|
|
|
:global(.dark) .virtual-scroll-container { |
|
border-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.event-index-item { |
|
margin-bottom: 1rem; |
|
padding-bottom: 1rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
.event-index-item:last-child { |
|
border-bottom: none; |
|
margin-bottom: 0; |
|
padding-bottom: 0; |
|
} |
|
|
|
.book-title { |
|
margin-top: 1rem; |
|
margin-bottom: 2rem; |
|
padding-bottom: 1rem; |
|
border-bottom: 2px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .book-title { |
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.publication-metadata { |
|
padding: 1rem; |
|
background: var(--fog-bg-secondary, #f9fafb); |
|
border-radius: 0.5rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .publication-metadata { |
|
background: var(--fog-dark-bg-secondary, #1f2937); |
|
border-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.topic-tag { |
|
display: inline-block; |
|
padding: 0.25rem 0.5rem; |
|
margin: 0.125rem; |
|
background: var(--fog-accent, #3b82f6); |
|
color: white; |
|
border-radius: 0.25rem; |
|
font-size: 0.875rem; |
|
} |
|
|
|
:global(.dark) .topic-tag { |
|
background: var(--fog-dark-accent, #2563eb); |
|
} |
|
|
|
.publication-cover-image { |
|
margin-top: 1rem; |
|
} |
|
|
|
.publication-cover-image img { |
|
max-width: 100%; |
|
height: auto; |
|
} |
|
|
|
.chapter-title { |
|
margin-top: 1.5rem; |
|
margin-bottom: 0.75rem; |
|
padding-bottom: 0.5rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
.chapter-title:first-child { |
|
margin-top: 0; |
|
} |
|
|
|
:global(.dark) .chapter-title { |
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.index-title { |
|
margin-top: 1.5rem; |
|
margin-bottom: 0.75rem; |
|
padding-bottom: 0.5rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
.index-title:first-child { |
|
margin-top: 0; |
|
} |
|
|
|
:global(.dark) .index-title { |
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.nested-index { |
|
border-left: 2px solid var(--fog-border, #e5e7eb); |
|
padding-left: 1rem; |
|
margin-top: 0.5rem; |
|
} |
|
|
|
:global(.dark) .nested-index { |
|
border-left-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.event-index-item[data-level="1"] { |
|
margin-left: 0.5rem; |
|
} |
|
|
|
.event-index-item[data-level="2"] { |
|
margin-left: 1rem; |
|
} |
|
|
|
.event-index-item[data-level="3"] { |
|
margin-left: 1.5rem; |
|
} |
|
|
|
:global(.dark) .event-index-item { |
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.event-index-list { |
|
max-height: none; |
|
} |
|
|
|
.error-message { |
|
color: var(--fog-error, #ef4444); |
|
padding: 1rem; |
|
background: var(--fog-error-bg, #fee2e2); |
|
border-radius: 0.5rem; |
|
} |
|
|
|
:global(.dark) .error-message { |
|
background: var(--fog-dark-error-bg, #7f1d1d); |
|
} |
|
|
|
.missing-events-section { |
|
padding: 1rem; |
|
background: var(--fog-warning-bg, #fef3c7); |
|
border: 1px solid var(--fog-warning-border, #fbbf24); |
|
border-radius: 0.5rem; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
:global(.dark) .missing-events-section { |
|
background: var(--fog-dark-warning-bg, #78350f); |
|
border-color: var(--fog-dark-warning-border, #d97706); |
|
} |
|
|
|
.missing-event-message { |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.missing-event-message:last-child { |
|
margin-bottom: 0; |
|
} |
|
|
|
.d-tag-code { |
|
font-family: monospace; |
|
background: var(--fog-code-bg, #f3f4f6); |
|
padding: 0.125rem 0.25rem; |
|
border-radius: 0.25rem; |
|
font-size: 0.875rem; |
|
} |
|
|
|
:global(.dark) .d-tag-code { |
|
background: var(--fog-dark-code-bg, #374151); |
|
} |
|
</style>
|
|
|