|
|
|
|
@ -1,325 +1,127 @@
@@ -1,325 +1,127 @@
|
|
|
|
|
<script lang="ts"> |
|
|
|
|
import { fade, slide } from 'svelte/transition'; |
|
|
|
|
import FeedPost from './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 { onMount } from 'svelte'; |
|
|
|
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
|
opEvent: NostrEvent | null; // The event that was clicked |
|
|
|
|
opEvent: NostrEvent | null; |
|
|
|
|
isOpen: boolean; |
|
|
|
|
onClose: () => void; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let { opEvent, isOpen, onClose }: Props = $props(); |
|
|
|
|
|
|
|
|
|
let drawerElement: HTMLElement | null = $state(null); |
|
|
|
|
let loading = $state(false); |
|
|
|
|
let rootEvent = $state<NostrEvent | null>(null); |
|
|
|
|
let rootReactions = $state<NostrEvent[]>([]); // Reactions for the root event |
|
|
|
|
let subscriptionId: string | null = $state(null); |
|
|
|
|
let isInitialized = $state(false); |
|
|
|
|
|
|
|
|
|
// Load root event and subscribe to updates when drawer opens |
|
|
|
|
$effect(() => { |
|
|
|
|
if (isOpen && opEvent) { |
|
|
|
|
// Hide main page scrollbar when drawer is open |
|
|
|
|
const originalOverflow = document.body.style.overflow; |
|
|
|
|
document.body.style.overflow = 'hidden'; |
|
|
|
|
|
|
|
|
|
loadRootEvent().then(() => { |
|
|
|
|
// Only subscribe after rootEvent is loaded |
|
|
|
|
if (rootEvent) { |
|
|
|
|
subscribeToUpdates(); |
|
|
|
|
// Initialize nostr client once |
|
|
|
|
onMount(async () => { |
|
|
|
|
if (!isInitialized) { |
|
|
|
|
await nostrClient.initialize(); |
|
|
|
|
isInitialized = true; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Cleanup on close |
|
|
|
|
return () => { |
|
|
|
|
document.body.style.overflow = originalOverflow; |
|
|
|
|
rootEvent = null; |
|
|
|
|
rootReactions = []; |
|
|
|
|
}; |
|
|
|
|
} else { |
|
|
|
|
// Reset when closed and restore scrollbar |
|
|
|
|
document.body.style.overflow = ''; |
|
|
|
|
rootEvent = null; |
|
|
|
|
rootReactions = []; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Find the root OP event by traversing up the reply chain |
|
|
|
|
* Uses a visited set to prevent infinite loops |
|
|
|
|
* Optimized to use cache-first lookup for speed |
|
|
|
|
*/ |
|
|
|
|
async function findRootEvent(event: NostrEvent, visited: Set<string> = new Set()): Promise<NostrEvent> { |
|
|
|
|
// Prevent infinite loops |
|
|
|
|
if (visited.has(event.id)) { |
|
|
|
|
return event; |
|
|
|
|
} |
|
|
|
|
visited.add(event.id); |
|
|
|
|
|
|
|
|
|
// Check for 'root' tag first (NIP-10) - this directly points to the root |
|
|
|
|
const rootTag = event.tags.find((t) => t[0] === 'root'); |
|
|
|
|
if (rootTag && rootTag[1]) { |
|
|
|
|
// If root tag points to self, we're already at root |
|
|
|
|
if (rootTag[1] === event.id) { |
|
|
|
|
return event; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Use getEventById which checks cache first, only hits network if not found |
|
|
|
|
const relays = relayManager.getFeedReadRelays(); |
|
|
|
|
const rootEvent = await nostrClient.getEventById(rootTag[1], relays); |
|
|
|
|
if (rootEvent) { |
|
|
|
|
return rootEvent; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Check if this event has a parent 'e' tag (NIP-10) |
|
|
|
|
// Look for 'e' tag with 'reply' marker, or any 'e' tag that's not self |
|
|
|
|
const eTags = event.tags.filter((t) => t[0] === 'e' && t[1] && t[1] !== event.id); |
|
|
|
|
|
|
|
|
|
// Prefer 'e' tag with 'reply' marker (NIP-10) |
|
|
|
|
let parentId: string | undefined; |
|
|
|
|
const replyTag = eTags.find((t) => t[3] === 'reply'); |
|
|
|
|
if (replyTag) { |
|
|
|
|
parentId = replyTag[1]; |
|
|
|
|
} else if (eTags.length > 0) { |
|
|
|
|
// Use first 'e' tag if no explicit reply marker |
|
|
|
|
parentId = eTags[0][1]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!parentId) { |
|
|
|
|
// No parent - this is the root |
|
|
|
|
return event; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Use getEventById which checks cache first, only hits network if not found |
|
|
|
|
const relays = relayManager.getFeedReadRelays(); |
|
|
|
|
const parent = await nostrClient.getEventById(parentId, relays); |
|
|
|
|
|
|
|
|
|
if (!parent) { |
|
|
|
|
// Parent not found - treat current event as root |
|
|
|
|
return event; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Recursively find root |
|
|
|
|
return findRootEvent(parent, visited); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadRootEvent() { |
|
|
|
|
if (!opEvent) return; |
|
|
|
|
|
|
|
|
|
// Always set loading initially to prevent empty panel |
|
|
|
|
loading = true; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Find the root OP event by traversing up the chain |
|
|
|
|
rootEvent = await findRootEvent(opEvent); |
|
|
|
|
|
|
|
|
|
if (!rootEvent) { |
|
|
|
|
// Fallback to opEvent if root not found |
|
|
|
|
rootEvent = opEvent; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Try to load reactions from cache first |
|
|
|
|
const reactionRelays = relayManager.getProfileReadRelays(); |
|
|
|
|
try { |
|
|
|
|
const cachedReactions = await nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [7], '#e': [rootEvent.id] }], |
|
|
|
|
reactionRelays, |
|
|
|
|
{ useCache: true, cacheResults: false, timeout: 100 } |
|
|
|
|
); |
|
|
|
|
if (cachedReactions.length > 0) { |
|
|
|
|
rootReactions = cachedReactions; |
|
|
|
|
loading = false; // Show content immediately with cached reactions |
|
|
|
|
// Load fresh reactions in background |
|
|
|
|
loadRootReactions(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
// Cache check failed, continue to full load |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// No cached reactions - load fresh |
|
|
|
|
await loadRootReactions(); |
|
|
|
|
loading = false; |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading root event:', error); |
|
|
|
|
// Ensure we have at least the opEvent to show |
|
|
|
|
if (!rootEvent && opEvent) { |
|
|
|
|
rootEvent = opEvent; |
|
|
|
|
} |
|
|
|
|
// Handle drawer open/close - only load when opening |
|
|
|
|
$effect(() => { |
|
|
|
|
if (isOpen && opEvent && isInitialized) { |
|
|
|
|
// Drawer opened - reset loading state |
|
|
|
|
loading = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadRootReactions() { |
|
|
|
|
if (!rootEvent) return; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const reactionRelays = relayManager.getProfileReadRelays(); |
|
|
|
|
const initialReactions = await nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [7], '#e': [rootEvent.id] }], |
|
|
|
|
reactionRelays, |
|
|
|
|
{ useCache: true, cacheResults: true } |
|
|
|
|
); |
|
|
|
|
rootReactions = initialReactions; |
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Error loading root reactions:', error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function subscribeToUpdates() { |
|
|
|
|
if (!rootEvent) return; |
|
|
|
|
|
|
|
|
|
const reactionRelays = relayManager.getProfileReadRelays(); |
|
|
|
|
const commentRelays = relayManager.getCommentReadRelays(); |
|
|
|
|
const zapRelays = relayManager.getZapReceiptReadRelays(); |
|
|
|
|
|
|
|
|
|
// Subscribe to reactions for the root event |
|
|
|
|
nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [7], '#e': [rootEvent.id] }], |
|
|
|
|
reactionRelays, |
|
|
|
|
{ |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
onUpdate: (updated: NostrEvent[]) => { |
|
|
|
|
// Batch updates to prevent flickering |
|
|
|
|
requestAnimationFrame(() => { |
|
|
|
|
// Add new reactions and update existing ones |
|
|
|
|
const existingIds = new Set(rootReactions.map(r => r.id)); |
|
|
|
|
const hasNew = updated.some(r => !existingIds.has(r.id)); |
|
|
|
|
|
|
|
|
|
if (hasNew) { |
|
|
|
|
// Only update if there are actual changes |
|
|
|
|
const updatedMap = new Map(rootReactions.map(r => [r.id, r])); |
|
|
|
|
for (const reaction of updated) { |
|
|
|
|
updatedMap.set(reaction.id, reaction); |
|
|
|
|
} |
|
|
|
|
rootReactions = Array.from(updatedMap.values()); |
|
|
|
|
// Cleanup subscription when drawer closes |
|
|
|
|
return () => { |
|
|
|
|
if (subscriptionId) { |
|
|
|
|
nostrClient.unsubscribe(subscriptionId); |
|
|
|
|
subscriptionId = null; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|
} else if (!isOpen) { |
|
|
|
|
// Drawer closed - cleanup |
|
|
|
|
if (subscriptionId) { |
|
|
|
|
nostrClient.unsubscribe(subscriptionId); |
|
|
|
|
subscriptionId = null; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
).catch(error => { |
|
|
|
|
console.error('Error subscribing to reactions:', error); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Subscribe to zap receipts for the root event |
|
|
|
|
nostrClient.fetchEvents( |
|
|
|
|
[{ kinds: [9735], '#e': [rootEvent.id] }], |
|
|
|
|
zapRelays, |
|
|
|
|
{ |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
onUpdate: (updated: NostrEvent[]) => { |
|
|
|
|
// Zap receipts are handled by FeedPost's internal subscription |
|
|
|
|
// This subscription ensures we get updates |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
).catch(error => { |
|
|
|
|
console.error('Error subscribing to zap receipts:', error); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Subscribe to comments/replies for the thread |
|
|
|
|
// CommentThread will handle its own updates, but we can also subscribe here |
|
|
|
|
nostrClient.fetchEvents( |
|
|
|
|
[ |
|
|
|
|
{ kinds: [1111], '#e': [rootEvent.id] }, |
|
|
|
|
{ kinds: [1111], '#E': [rootEvent.id] }, |
|
|
|
|
{ kinds: [1], '#e': [rootEvent.id] }, |
|
|
|
|
{ kinds: [1244], '#e': [rootEvent.id] }, |
|
|
|
|
{ kinds: [9735], '#e': [rootEvent.id] } |
|
|
|
|
], |
|
|
|
|
commentRelays, |
|
|
|
|
{ |
|
|
|
|
useCache: true, |
|
|
|
|
cacheResults: true, |
|
|
|
|
onUpdate: (updated: NostrEvent[]) => { |
|
|
|
|
// CommentThread will handle these updates via its own subscription |
|
|
|
|
// This ensures we get updates even if CommentThread hasn't loaded yet |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
).catch(error => { |
|
|
|
|
console.error('Error subscribing to comments:', error); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function handleBackdropClick(e: MouseEvent) { |
|
|
|
|
if (e.target === e.currentTarget) { |
|
|
|
|
// Handle keyboard events |
|
|
|
|
function handleKeyDown(e: KeyboardEvent) { |
|
|
|
|
if (e.key === 'Escape' && isOpen) { |
|
|
|
|
onClose(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function handleEscape(e: KeyboardEvent) { |
|
|
|
|
if (e.key === 'Escape') { |
|
|
|
|
// Handle backdrop click |
|
|
|
|
function handleBackdropClick(e: MouseEvent) { |
|
|
|
|
if (e.target === e.currentTarget) { |
|
|
|
|
onClose(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function handleBackdropWheel(e: WheelEvent) { |
|
|
|
|
// Only prevent scrolling if the event target is the backdrop itself |
|
|
|
|
// Allow scrolling on the drawer content |
|
|
|
|
const target = e.target as HTMLElement; |
|
|
|
|
if (target && target.classList.contains('drawer-backdrop')) { |
|
|
|
|
e.preventDefault(); |
|
|
|
|
e.stopPropagation(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function handleBackdropTouchMove(e: TouchEvent) { |
|
|
|
|
// Only prevent touch scrolling if the event target is the backdrop itself |
|
|
|
|
const target = e.target as HTMLElement; |
|
|
|
|
if (target && target.classList.contains('drawer-backdrop')) { |
|
|
|
|
e.preventDefault(); |
|
|
|
|
e.stopPropagation(); |
|
|
|
|
} |
|
|
|
|
// Prevent body scroll when drawer is open |
|
|
|
|
$effect(() => { |
|
|
|
|
if (isOpen) { |
|
|
|
|
document.body.style.overflow = 'hidden'; |
|
|
|
|
return () => { |
|
|
|
|
document.body.style.overflow = ''; |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<svelte:window onkeydown={handleEscape} /> |
|
|
|
|
|
|
|
|
|
{#if isOpen} |
|
|
|
|
{#if isOpen && opEvent} |
|
|
|
|
<div |
|
|
|
|
class="drawer-backdrop" |
|
|
|
|
onclick={handleBackdropClick} |
|
|
|
|
onwheel={handleBackdropWheel} |
|
|
|
|
ontouchmove={handleBackdropTouchMove} |
|
|
|
|
onkeydown={(e) => { |
|
|
|
|
if (e.key === 'Escape') { |
|
|
|
|
onClose(); |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
onkeydown={handleKeyDown} |
|
|
|
|
role="button" |
|
|
|
|
tabindex="0" |
|
|
|
|
aria-label="Close drawer" |
|
|
|
|
transition:fade={{ duration: 200 }} |
|
|
|
|
aria-label="Close thread drawer" |
|
|
|
|
></div> |
|
|
|
|
<div |
|
|
|
|
class="thread-drawer drawer-right" |
|
|
|
|
bind:this={drawerElement} |
|
|
|
|
onkeydown={handleKeyDown} |
|
|
|
|
role="dialog" |
|
|
|
|
aria-modal="true" |
|
|
|
|
aria-label="Thread drawer" |
|
|
|
|
tabindex="-1" |
|
|
|
|
> |
|
|
|
|
<div class="drawer" transition:slide={{ axis: 'x', duration: 300 }}> |
|
|
|
|
<div class="drawer-header"> |
|
|
|
|
<h2 class="drawer-title">Thread</h2> |
|
|
|
|
<button class="close-button" onclick={onClose} aria-label="Close drawer"> |
|
|
|
|
<h3 class="drawer-title">Thread</h3> |
|
|
|
|
<button |
|
|
|
|
onclick={onClose} |
|
|
|
|
class="drawer-close" |
|
|
|
|
aria-label="Close thread drawer" |
|
|
|
|
title="Close" |
|
|
|
|
> |
|
|
|
|
× |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div class="drawer-content"> |
|
|
|
|
{#if loading && !rootEvent} |
|
|
|
|
{#if loading} |
|
|
|
|
<div class="loading-state"> |
|
|
|
|
<p class="text-fog-text-light dark:text-fog-dark-text-light">Loading thread...</p> |
|
|
|
|
{:else if rootEvent} |
|
|
|
|
<!-- Display the root OP event --> |
|
|
|
|
<div class="op-section"> |
|
|
|
|
<FeedPost post={rootEvent} reactions={rootReactions} /> |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<div class="thread-content"> |
|
|
|
|
<!-- Display the OP event --> |
|
|
|
|
<div class="op-post"> |
|
|
|
|
<FeedPost post={opEvent} /> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<!-- Display all replies using CommentThread --> |
|
|
|
|
<div class="replies-section"> |
|
|
|
|
<CommentThread threadId={rootEvent.id} event={rootEvent} /> |
|
|
|
|
<!-- Display comments/replies --> |
|
|
|
|
<div class="comments-section"> |
|
|
|
|
<CommentThread threadId={opEvent.id} event={opEvent} /> |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<p class="text-fog-text-light dark:text-fog-dark-text-light">Unable to load thread.</p> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
@ -332,36 +134,42 @@
@@ -332,36 +134,42 @@
|
|
|
|
|
right: 0; |
|
|
|
|
bottom: 0; |
|
|
|
|
background: rgba(0, 0, 0, 0.5); |
|
|
|
|
z-index: 1000; |
|
|
|
|
display: flex; |
|
|
|
|
justify-content: flex-end; |
|
|
|
|
overflow: hidden; |
|
|
|
|
overscroll-behavior: contain; |
|
|
|
|
z-index: 999; |
|
|
|
|
animation: fadeIn 0.3s ease-out; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Allow scrolling on drawer content */ |
|
|
|
|
.drawer-content { |
|
|
|
|
touch-action: auto; |
|
|
|
|
@keyframes fadeIn { |
|
|
|
|
from { |
|
|
|
|
opacity: 0; |
|
|
|
|
} |
|
|
|
|
to { |
|
|
|
|
opacity: 1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .drawer-backdrop { |
|
|
|
|
background: rgba(0, 0, 0, 0.7); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.drawer { |
|
|
|
|
width: var(--content-width, 800px); |
|
|
|
|
max-width: 100vw; |
|
|
|
|
height: 100%; |
|
|
|
|
.thread-drawer { |
|
|
|
|
position: fixed; |
|
|
|
|
top: 0; |
|
|
|
|
right: 0; |
|
|
|
|
bottom: 0; |
|
|
|
|
width: min(600px, 90vw); |
|
|
|
|
max-width: 600px; |
|
|
|
|
background: var(--fog-post, #ffffff); |
|
|
|
|
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); |
|
|
|
|
border-left: 2px solid var(--fog-border, #cbd5e1); |
|
|
|
|
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2); |
|
|
|
|
padding: 0; |
|
|
|
|
z-index: 1000; |
|
|
|
|
display: flex; |
|
|
|
|
flex-direction: column; |
|
|
|
|
overflow: hidden; |
|
|
|
|
animation: slideInRight 0.3s ease-out; |
|
|
|
|
transform: translateX(0); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .drawer { |
|
|
|
|
:global(.dark) .thread-drawer { |
|
|
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
|
|
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3); |
|
|
|
|
border-left-color: var(--fog-dark-border, #475569); |
|
|
|
|
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.drawer-header { |
|
|
|
|
@ -370,6 +178,7 @@
@@ -370,6 +178,7 @@
|
|
|
|
|
align-items: center; |
|
|
|
|
padding: 1rem; |
|
|
|
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
flex-shrink: 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .drawer-header { |
|
|
|
|
@ -377,56 +186,81 @@
@@ -377,56 +186,81 @@
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.drawer-title { |
|
|
|
|
font-size: 1.25rem; |
|
|
|
|
font-weight: 600; |
|
|
|
|
margin: 0; |
|
|
|
|
color: var(--fog-text, #111827); |
|
|
|
|
font-size: 1.125rem; |
|
|
|
|
font-weight: 600; |
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .drawer-title { |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.close-button { |
|
|
|
|
background: none; |
|
|
|
|
.drawer-close { |
|
|
|
|
background: transparent; |
|
|
|
|
border: none; |
|
|
|
|
font-size: 2rem; |
|
|
|
|
font-size: 1.5rem; |
|
|
|
|
line-height: 1; |
|
|
|
|
cursor: pointer; |
|
|
|
|
color: var(--fog-text-light, #6b7280); |
|
|
|
|
padding: 0; |
|
|
|
|
width: 2rem; |
|
|
|
|
height: 2rem; |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
justify-content: center; |
|
|
|
|
color: var(--fog-text-light, #9ca3af); |
|
|
|
|
padding: 0.25rem 0.5rem; |
|
|
|
|
border-radius: 0.25rem; |
|
|
|
|
transition: all 0.2s; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.drawer-close:hover { |
|
|
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.close-button:hover { |
|
|
|
|
color: var(--fog-text, #111827); |
|
|
|
|
:global(.dark) .drawer-close { |
|
|
|
|
color: var(--fog-dark-text-light, #6b7280); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .close-button:hover { |
|
|
|
|
:global(.dark) .drawer-close:hover { |
|
|
|
|
background: var(--fog-dark-highlight, #374151); |
|
|
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@keyframes slideInRight { |
|
|
|
|
from { |
|
|
|
|
transform: translateX(100%); |
|
|
|
|
} |
|
|
|
|
to { |
|
|
|
|
transform: translateX(0); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.drawer-content { |
|
|
|
|
flex: 1; |
|
|
|
|
overflow-y: auto; |
|
|
|
|
padding: 1rem; |
|
|
|
|
overflow-x: hidden; |
|
|
|
|
flex: 1; |
|
|
|
|
padding: 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.loading-state { |
|
|
|
|
padding: 2rem; |
|
|
|
|
text-align: center; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.op-section { |
|
|
|
|
margin-bottom: 2rem; |
|
|
|
|
padding-bottom: 1rem; |
|
|
|
|
.thread-content { |
|
|
|
|
display: flex; |
|
|
|
|
flex-direction: column; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.op-post { |
|
|
|
|
padding: 1rem; |
|
|
|
|
border-bottom: 2px solid var(--fog-border, #e5e7eb); |
|
|
|
|
flex-shrink: 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .op-section { |
|
|
|
|
:global(.dark) .op-post { |
|
|
|
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.replies-section { |
|
|
|
|
margin-top: 2rem; |
|
|
|
|
.comments-section { |
|
|
|
|
padding: 1rem; |
|
|
|
|
flex: 1; |
|
|
|
|
min-height: 0; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
|