Browse Source

bug-fixes

master
Silberengel 1 month ago
parent
commit
1d54143cb6
  1. 20
      src/lib/components/content/MarkdownRenderer.svelte
  2. 13
      src/lib/modules/comments/CommentForm.svelte
  3. 106
      src/lib/modules/comments/CommentThread.svelte
  4. 32
      src/lib/modules/feed/ThreadDrawer.svelte
  5. 23
      src/lib/modules/profiles/ProfilePage.svelte
  6. 259
      src/lib/modules/threads/ThreadView.svelte

20
src/lib/components/content/MarkdownRenderer.svelte

@ -213,10 +213,14 @@ @@ -213,10 +213,14 @@
}
// Replace placeholder - it will be in a <code> tag after markdown parsing
const codePlaceholder = `<code>${placeholder.replace(/`/g, '')}</code>`;
finalHtml = finalHtml.replace(new RegExp(codePlaceholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), replacement);
// The placeholder is like `NIP21PLACEHOLDER0`, which becomes <code>NIP21PLACEHOLDER0</code>
const placeholderText = placeholder.replace(/`/g, ''); // Remove backticks
const codePlaceholder = `<code>${placeholderText}</code>`;
// Escape special regex characters
const escapedCodePlaceholder = codePlaceholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
finalHtml = finalHtml.replace(new RegExp(escapedCodePlaceholder, 'g'), replacement);
// Also try without code tag (in case markdown didn't process it)
const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedPlaceholder = placeholderText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
finalHtml = finalHtml.replace(new RegExp(escapedPlaceholder, 'g'), replacement);
}
@ -301,8 +305,14 @@ @@ -301,8 +305,14 @@
}
}
// Escape placeholder for regex replacement
const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Replace placeholder - it will be in a <code> tag after markdown parsing
const placeholderText = placeholder.replace(/`/g, ''); // Remove backticks
const codePlaceholder = `<code>${placeholderText}</code>`;
// Escape special regex characters
const escapedCodePlaceholder = codePlaceholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
finalHtml = finalHtml.replace(new RegExp(escapedCodePlaceholder, 'g'), replacement);
// Also try without code tag (in case markdown didn't process it)
const escapedPlaceholder = placeholderText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
finalHtml = finalHtml.replace(new RegExp(escapedPlaceholder, 'g'), replacement);
}

13
src/lib/modules/comments/CommentForm.svelte

@ -134,11 +134,11 @@ @@ -134,11 +134,11 @@
></textarea>
<div class="flex items-center justify-between mt-2">
<label class="flex items-center gap-2 text-sm text-fog-text dark:text-fog-dark-text">
<label class="flex items-center gap-2 text-sm text-fog-text-light dark:text-fog-dark-text-light opacity-70">
<input
type="checkbox"
bind:checked={includeClientTag}
class="rounded"
class="rounded client-tag-checkbox"
/>
Include client tag
</label>
@ -178,4 +178,13 @@ @@ -178,4 +178,13 @@
outline: none;
border-color: var(--fog-accent, #64748b);
}
.client-tag-checkbox {
opacity: 0.7;
cursor: pointer;
}
.client-tag-checkbox:hover {
opacity: 0.9;
}
</style>

106
src/lib/modules/comments/CommentThread.svelte

@ -21,6 +21,7 @@ @@ -21,6 +21,7 @@
let zapReceipts = $state<NostrEvent[]>([]); // kind 9735 (zap receipts)
let loading = $state(true);
let replyingTo = $state<NostrEvent | null>(null);
let loadingPromise: Promise<void> | null = null; // Track ongoing load to prevent concurrent calls
const isKind1 = $derived(event?.kind === 1);
const rootKind = $derived(event?.kind || null);
@ -32,7 +33,27 @@ @@ -32,7 +33,27 @@
// Reload comments when threadId or event changes
$effect(() => {
if (threadId) {
loadComments();
// Access event to make it a dependency of this effect
// This ensures the effect re-runs when event changes from undefined to the actual event
const currentEvent = event;
const currentIsKind1 = isKind1;
// Prevent concurrent loads
if (loadingPromise) {
return;
}
// Load comments - filters will adapt based on whether event is available
// Ensure nostrClient is initialized first
loadingPromise = nostrClient.initialize().then(() => {
return loadComments();
}).catch((error) => {
console.error('Error initializing nostrClient in CommentThread:', error);
// Still try to load comments even if initialization fails
return loadComments();
}).finally(() => {
loadingPromise = null;
});
}
});
@ -82,8 +103,12 @@ @@ -82,8 +103,12 @@
// For kind 1, 1244, 9735: check e tag
if (replyEvent.kind === 1 || replyEvent.kind === 1244 || replyEvent.kind === 9735) {
const eTag = replyEvent.tags.find((t) => t[0] === 'e' && t[1] !== replyEvent.id);
if (eTag && eTag[1]) return eTag[1];
// For kind 1, check all e tags (NIP-10)
const eTags = replyEvent.tags.filter((t) => t[0] === 'e' && t[1] && t[1] !== replyEvent.id);
// Prefer e tag with 'reply' marker, otherwise use first e tag
const replyTag = eTags.find((t) => t[3] === 'reply');
if (replyTag && replyTag[1]) return replyTag[1];
if (eTags.length > 0 && eTags[0][1]) return eTags[0][1];
}
return null;
@ -127,11 +152,16 @@ @@ -127,11 +152,16 @@
}
async function loadComments() {
if (!threadId) {
loading = false;
return;
}
loading = true;
try {
const relays = relayManager.getCommentReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const allRelays = [...new Set([...relays, ...feedRelays])];
// Use all relay sources: profileRelays + defaultRelays + user's inboxes + user's localrelays + cache
// getProfileReadRelays() includes: defaultRelays + profileRelays + user inbox (which includes local relays from kind 10432)
const allRelays = relayManager.getProfileReadRelays();
const replyFilters: any[] = [];
@ -153,23 +183,34 @@ @@ -153,23 +183,34 @@
// Fetch zap receipts (kind 9735)
replyFilters.push({ kinds: [9735], '#e': [threadId] });
console.log('CommentThread: Loading comments for threadId:', threadId, 'event kind:', event?.kind);
console.log('CommentThread: Filters:', replyFilters);
const allReplies = await nostrClient.fetchEvents(
replyFilters,
allRelays,
{ useCache: true, cacheResults: true }
{ useCache: true, cacheResults: true, timeout: 10000 }
);
console.log('CommentThread: Fetched', allReplies.length, 'replies');
// Filter to only replies that reference the root
const rootReplies = allReplies.filter(reply => referencesRoot(reply));
console.log('CommentThread: Root replies:', rootReplies.length);
// Separate by type
comments = rootReplies.filter(e => e.kind === 1111);
kind1Replies = rootReplies.filter(e => e.kind === 1);
yakBacks = rootReplies.filter(e => e.kind === 1244);
zapReceipts = rootReplies.filter(e => e.kind === 9735);
// Recursively fetch all nested replies
await fetchNestedReplies();
console.log('CommentThread: Separated - comments:', comments.length, 'kind1Replies:', kind1Replies.length, 'yakBacks:', yakBacks.length, 'zapReceipts:', zapReceipts.length);
// Recursively fetch all nested replies (non-blocking - let it run in background)
fetchNestedReplies().catch((error) => {
console.error('Error fetching nested replies:', error);
});
} catch (error) {
console.error('Error loading comments:', error);
@ -179,9 +220,8 @@ @@ -179,9 +220,8 @@
}
async function fetchNestedReplies() {
const relays = relayManager.getCommentReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const allRelays = [...new Set([...relays, ...feedRelays])];
// Use all relay sources: profileRelays + defaultRelays + user's inboxes + user's localrelays + cache
const allRelays = relayManager.getProfileReadRelays();
let hasNewReplies = true;
let iterations = 0;
const maxIterations = 10;
@ -272,16 +312,32 @@ @@ -272,16 +312,32 @@
// Second pass: determine parent-child relationships
for (const item of items) {
const parentId = getParentEventId(item.event);
if (parentId && (parentId === threadId || allEventIds.has(parentId))) {
// This is a reply
console.log('CommentThread: sortThreadItems - item:', item.type, item.event.id.slice(0, 8), 'parentId:', parentId ? parentId.slice(0, 8) : 'null', 'threadId:', threadId.slice(0, 8));
if (parentId) {
if (parentId === threadId) {
// This is a direct reply to the root OP
if (!replyMap.has(threadId)) {
replyMap.set(threadId, []);
}
replyMap.get(threadId)!.push(item.event.id);
console.log('CommentThread: Added to replyMap for root threadId:', threadId.slice(0, 8));
} else if (allEventIds.has(parentId)) {
// This is a reply to another reply
if (!replyMap.has(parentId)) {
replyMap.set(parentId, []);
}
replyMap.get(parentId)!.push(item.event.id);
console.log('CommentThread: Added to replyMap for parent:', parentId.slice(0, 8));
} else {
// Parent not found - treat as root item (orphaned reply)
rootItems.push(item);
console.log('CommentThread: Added to rootItems (orphaned)');
}
} else {
// No parent or parent not found - treat as root item
// No parent - treat as root item (direct reply without parent tag)
rootItems.push(item);
console.log('CommentThread: Added to rootItems (no parent)');
}
}
@ -307,7 +363,18 @@ @@ -307,7 +363,18 @@
}
}
// Add all root items sorted by time
// First, add all direct replies to the root OP (threadId)
const rootReplies = replyMap.get(threadId) || [];
const rootReplyItems = rootReplies
.map(id => eventMap.get(id))
.filter((item): item is { event: NostrEvent; type: 'comment' | 'reply' | 'yak' | 'zap' } => item !== undefined)
.sort((a, b) => a.event.created_at - b.event.created_at);
for (const reply of rootReplyItems) {
addThread(reply);
}
// Then, add all root items (orphaned replies) sorted by time
rootItems.sort((a, b) => a.event.created_at - b.event.created_at);
for (const root of rootItems) {
addThread(root);
@ -323,7 +390,10 @@ @@ -323,7 +390,10 @@
...yakBacks.map(y => ({ event: y, type: 'yak' as const })),
...zapReceipts.map(z => ({ event: z, type: 'zap' as const }))
];
return sortThreadItems(items);
console.log('CommentThread: getThreadItems - items before sort:', items.length, items.map(i => ({ type: i.type, id: i.event.id.slice(0, 8) })));
const sorted = sortThreadItems(items);
console.log('CommentThread: getThreadItems - items after sort:', sorted.length, sorted.map(i => ({ type: i.type, id: i.event.id.slice(0, 8) })));
return sorted;
}
function handleReply(replyEvent: NostrEvent) {

32
src/lib/modules/feed/ThreadDrawer.svelte

@ -1,14 +1,13 @@ @@ -1,14 +1,13 @@
<script lang="ts">
import { fade, slide } from 'svelte/transition';
import FeedPost from './FeedPost.svelte';
import FeedReactionButtons from '../reactions/FeedReactionButtons.svelte';
import CommentThread from '../comments/CommentThread.svelte';
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js';
import type { NostrEvent } from '../../types/nostr.js';
interface Props {
opEvent: NostrEvent | null; // The original post/event
opEvent: NostrEvent | null; // The event that was clicked
isOpen: boolean;
onClose: () => void;
}
@ -16,7 +15,7 @@ @@ -16,7 +15,7 @@
let { opEvent, isOpen, onClose }: Props = $props();
let loading = $state(false);
let rootEvent = $state<NostrEvent | null>(null); // The actual OP/root event
let rootEvent = $state<NostrEvent | null>(null);
// Load root event when drawer opens
$effect(() => {
@ -101,20 +100,15 @@ @@ -101,20 +100,15 @@
loading = true;
try {
const relays = relayManager.getFeedReadRelays();
// First, find the root OP event
// Find the root OP event by traversing up the chain
rootEvent = await findRootEvent(opEvent);
// Root event is now loaded, CommentThread will handle loading replies
} catch (error) {
console.error('Error loading thread:', error);
console.error('Error loading root event:', error);
} finally {
loading = false;
}
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose();
@ -156,15 +150,12 @@ @@ -156,15 +150,12 @@
{#if loading}
<p class="text-fog-text-light dark:text-fog-dark-text-light">Loading thread...</p>
{:else if rootEvent}
<!-- OP with reactions -->
<!-- Display the root OP event -->
<div class="op-section">
<FeedPost post={rootEvent} />
<div class="reactions-section">
<FeedReactionButtons event={rootEvent} />
</div>
</div>
<!-- Threaded replies using CommentThread -->
<!-- Display all replies using CommentThread -->
<div class="replies-section">
<CommentThread threadId={rootEvent.id} event={rootEvent} />
</div>
@ -269,18 +260,7 @@ @@ -269,18 +260,7 @@
border-bottom-color: var(--fog-dark-border, #374151);
}
.reactions-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .reactions-section {
border-top-color: var(--fog-dark-border, #374151);
}
.replies-section {
margin-top: 2rem;
}
</style>

23
src/lib/modules/profiles/ProfilePage.svelte

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte';
import PaymentAddresses from './PaymentAddresses.svelte';
import FeedPost from '../feed/FeedPost.svelte';
import ThreadDrawer from '../feed/ThreadDrawer.svelte';
import { fetchProfile, fetchUserStatus, type ProfileData } from '../../services/user-data.js';
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js';
@ -24,6 +25,20 @@ @@ -24,6 +25,20 @@
// Get current logged-in user's pubkey
let currentUserPubkey = $state<string | null>(sessionManager.getCurrentPubkey());
// Drawer state for viewing threads
let drawerOpen = $state(false);
let drawerEvent = $state<NostrEvent | null>(null);
function openDrawer(event: NostrEvent) {
drawerEvent = event;
drawerOpen = true;
}
function closeDrawer() {
drawerOpen = false;
drawerEvent = null;
}
// Subscribe to session changes
$effect(() => {
const unsubscribe = sessionManager.session.subscribe((session) => {
@ -371,7 +386,7 @@ @@ -371,7 +386,7 @@
{:else}
<div class="posts-list">
{#each posts as post (post.id)}
<FeedPost {post} />
<FeedPost post={post} onOpenEvent={openDrawer} />
{/each}
</div>
{/if}
@ -381,7 +396,7 @@ @@ -381,7 +396,7 @@
{:else}
<div class="responses-list">
{#each responses as response (response.id)}
<FeedPost post={response} />
<FeedPost post={response} onOpenEvent={openDrawer} />
{/each}
</div>
{/if}
@ -391,7 +406,7 @@ @@ -391,7 +406,7 @@
{:else}
<div class="interactions-list">
{#each interactionsWithMe as interaction (interaction.id)}
<FeedPost post={interaction} />
<FeedPost post={interaction} onOpenEvent={openDrawer} />
{/each}
</div>
{/if}
@ -400,6 +415,8 @@ @@ -400,6 +415,8 @@
{:else}
<p class="text-fog-text-light dark:text-fog-dark-text-light">Profile not found</p>
{/if}
<ThreadDrawer opEvent={drawerEvent} isOpen={drawerOpen} onClose={closeDrawer} />
</div>
<style>

259
src/lib/modules/threads/ThreadView.svelte

@ -1,16 +1,10 @@ @@ -1,16 +1,10 @@
<script lang="ts">
import ProfileBadge from '../../components/layout/ProfileBadge.svelte';
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte';
import MediaAttachments from '../../components/content/MediaAttachments.svelte';
import FeedPost from '../feed/FeedPost.svelte';
import CommentThread from '../comments/CommentThread.svelte';
import ReactionButtons from '../reactions/ReactionButtons.svelte';
import ZapButton from '../zaps/ZapButton.svelte';
import ZapReceipt from '../zaps/ZapReceipt.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';
import { getKindInfo } from '../../types/kind-lookup.js';
interface Props {
threadId: string;
@ -18,158 +12,108 @@ @@ -18,158 +12,108 @@
let { threadId }: Props = $props();
let thread = $state<NostrEvent | null>(null);
let rootEvent = $state<NostrEvent | null>(null);
let loading = $state(true);
let expanded = $state(false);
let contentElement: HTMLElement | null = $state(null);
let needsExpansion = $state(false);
onMount(async () => {
await nostrClient.initialize();
loadThread();
loadRootEvent();
});
$effect(() => {
if (threadId) {
loadThread();
loadRootEvent();
}
});
async function loadThread() {
loading = true;
try {
// Try multiple relay sets to find the event
const threadRelays = relayManager.getThreadReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const allRelays = [...new Set([...threadRelays, ...feedRelays])];
const event = await nostrClient.getEventById(threadId, allRelays);
thread = event;
} catch (error) {
console.error('Error loading thread:', error);
} finally {
loading = false;
}
/**
* Find the root OP event by traversing up the reply chain
*/
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);
function getTitle(): string {
if (!thread) return '';
// For kind 11 threads, use title tag
if (thread.kind === 11) {
const titleTag = thread.tags.find((t) => t[0] === 'title');
return titleTag?.[1] || 'Untitled';
// 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;
}
// For other kinds, show kind description or first line of content
const firstLine = thread.content.split('\n')[0].trim();
if (firstLine.length > 0 && firstLine.length < 100) {
return firstLine;
const relays = relayManager.getFeedReadRelays();
const rootEvent = await nostrClient.getEventById(rootTag[1], relays);
if (rootEvent) {
return rootEvent;
}
return getKindInfo(thread.kind).description;
}
function getTopics(): string[] {
if (!thread) return [];
return thread.tags.filter((t) => t[0] === 't').map((t) => t[1]).slice(0, 3);
// Check if this event has a parent 'e' tag (NIP-10)
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];
}
function getClientName(): string | null {
if (!thread) return null;
const clientTag = thread.tags.find((t) => t[0] === 'client');
return clientTag?.[1] || null;
if (!parentId) {
// No parent - this is the root
return event;
}
function getRelativeTime(): string {
if (!thread) return '';
const now = Math.floor(Date.now() / 1000);
const diff = now - thread.created_at;
const hours = Math.floor(diff / 3600);
const days = Math.floor(diff / 86400);
const relays = relayManager.getFeedReadRelays();
const parent = await nostrClient.getEventById(parentId, relays);
if (days > 0) return `${days}d ago`;
if (hours > 0) return `${hours}h ago`;
return 'just now';
if (!parent) {
// Parent not found - treat current event as root
return event;
}
$effect(() => {
if (contentElement && thread) {
checkContentHeight();
// Use ResizeObserver to detect when content changes (e.g., images loading)
const observer = new ResizeObserver(() => {
checkContentHeight();
});
observer.observe(contentElement);
return () => observer.disconnect();
// Recursively find root
return findRootEvent(parent, visited);
}
});
function checkContentHeight() {
if (contentElement) {
// Use requestAnimationFrame to ensure DOM is fully updated
requestAnimationFrame(() => {
if (contentElement) {
needsExpansion = contentElement.scrollHeight > 500;
}
});
async function loadRootEvent() {
loading = true;
try {
const threadRelays = relayManager.getThreadReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const allRelays = [...new Set([...threadRelays, ...feedRelays])];
// Load the event by ID
const event = await nostrClient.getEventById(threadId, allRelays);
if (event) {
// Find the root OP by traversing up the chain
rootEvent = await findRootEvent(event);
}
} catch (error) {
console.error('Error loading thread:', error);
} finally {
loading = false;
}
function toggleExpanded() {
expanded = !expanded;
}
</script>
{#if loading}
<p class="text-fog-text dark:text-fog-dark-text">Loading thread...</p>
{:else if thread}
{:else if rootEvent}
<article class="thread-view">
<div class="thread-header mb-4">
<h1 class="text-2xl font-bold mb-2">{getTitle()}</h1>
<div class="flex items-center gap-2 mb-2">
<ProfileBadge pubkey={thread.pubkey} />
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
{#if getClientName()}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span>
{/if}
<!-- Display the root OP event -->
<div class="op-section">
<FeedPost post={rootEvent} />
</div>
{#if getTopics().length > 0}
<div class="flex gap-2 mb-2">
{#each getTopics() as topic}
<span class="text-xs bg-fog-highlight dark:bg-fog-dark-highlight text-fog-text dark:text-fog-dark-text px-2 py-1 rounded">{topic}</span>
{/each}
</div>
{/if}
</div>
<div class="card-content" class:expanded bind:this={contentElement}>
<div class="thread-content mb-4">
<MediaAttachments event={thread} />
<MarkdownRenderer content={thread.content} />
</div>
<div class="thread-actions flex items-center justify-between gap-4 mb-6">
<div class="flex items-center gap-4">
<ReactionButtons event={thread} />
<ZapButton event={thread} />
<ZapReceipt eventId={thread.id} pubkey={thread.pubkey} />
</div>
<div class="kind-badge">
<span class="kind-number">{getKindInfo(thread.kind).number}</span>
<span class="kind-description">{getKindInfo(thread.kind).description}</span>
</div>
</div>
</div>
{#if needsExpansion}
<button
onclick={toggleExpanded}
class="show-more-button text-sm text-fog-accent dark:text-fog-dark-accent hover:underline mt-2"
>
{expanded ? 'Show less' : 'Show more'}
</button>
{/if}
<!-- Display all replies using CommentThread -->
<div class="comments-section">
<CommentThread threadId={thread.id} event={thread} />
<CommentThread threadId={rootEvent.id} event={rootEvent} />
</div>
</article>
{:else}
@ -180,77 +124,20 @@ @@ -180,77 +124,20 @@
.thread-view {
max-width: var(--content-width);
margin: 0 auto;
position: relative;
}
.thread-content {
line-height: 1.6;
}
.thread-actions {
padding-top: 1rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
padding: 1rem;
}
.thread-actions > div {
display: flex;
align-items: center;
.op-section {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .thread-actions {
border-top-color: var(--fog-dark-border, #374151);
:global(.dark) .op-section {
border-bottom-color: var(--fog-dark-border, #374151);
}
.comments-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .comments-section {
border-top-color: var(--fog-dark-border, #374151);
}
.card-content {
max-height: 500px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.card-content.expanded {
max-height: none;
}
.show-more-button {
width: 100%;
text-align: center;
padding: 0.5rem;
background: transparent;
border: none;
cursor: pointer;
}
.kind-badge {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
line-height: 1;
color: var(--fog-text-light, #9ca3af);
flex-shrink: 0;
}
:global(.dark) .kind-badge {
color: var(--fog-dark-text-light, #6b7280);
}
.kind-number {
font-weight: 600;
}
.kind-description {
font-size: 0.625rem;
opacity: 0.8;
}
</style>

Loading…
Cancel
Save