diff --git a/src/app.css b/src/app.css index 98f41a0..1105ec0 100644 --- a/src/app.css +++ b/src/app.css @@ -67,12 +67,46 @@ body { background-color: #f1f5f9; color: #475569; /* WCAG AA compliant: 5.2:1 contrast ratio */ transition: background-color 0.3s ease, color 0.3s ease; + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace; +} + +/* Secret supercoder vibe - subtle terminal aesthetic */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: + repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.03) 2px, + rgba(0, 0, 0, 0.03) 4px + ); + pointer-events: none; + z-index: 9999; + opacity: 0.5; +} + +.dark body::before { + background: + repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(255, 255, 255, 0.02) 2px, + rgba(255, 255, 255, 0.02) 4px + ); } /* Dark mode body styles */ .dark body { - background-color: #0f172a; + background-color: #0a0e1a; color: #cbd5e1; /* WCAG AA compliant: 13.5:1 contrast ratio */ + text-shadow: 0 0 1px rgba(0, 255, 0, 0.1); } /* Fog aesthetic base styles */ diff --git a/src/lib/components/content/GifPicker.svelte b/src/lib/components/content/GifPicker.svelte index 75229ad..be65233 100644 --- a/src/lib/components/content/GifPicker.svelte +++ b/src/lib/components/content/GifPicker.svelte @@ -274,10 +274,8 @@ let errorCount = 0; try { - const relays = relayManager.getPublishRelays( - [...relayManager.getThreadReadRelays(), ...relayManager.getFeedReadRelays()], - true - ); + // For kind 1063, use file metadata publish relays (includes GIF relays) + const relays = relayManager.getFileMetadataPublishRelays(); // Process each selected file for (const file of Array.from(files)) { @@ -408,10 +406,8 @@ uploadError = null; try { - const relays = relayManager.getPublishRelays( - [...relayManager.getThreadReadRelays(), ...relayManager.getFeedReadRelays()], - true - ); + // For kind 1063, use file metadata publish relays (includes GIF relays) + const relays = relayManager.getFileMetadataPublishRelays(); // Build tags array with metadata const tags: string[][] = [ diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte index 67d4ee2..f023c16 100644 --- a/src/lib/components/content/MarkdownRenderer.svelte +++ b/src/lib/components/content/MarkdownRenderer.svelte @@ -221,10 +221,38 @@ }); } + // Convert greentext (>text with no space) to styled spans + function convertGreentext(text: string): string { + // Split by lines and process each line + const lines = text.split('\n'); + const processedLines = lines.map(line => { + // Check if line starts with > followed immediately by non-whitespace (greentext) + // Must match: >text (no space after >) + // Must NOT match: > text (space after >, normal blockquote) + // Also handle HTML-escaped > (>) + const greentextPattern = /^(>|>)([^\s>].*)$/; + const match = line.match(greentextPattern); + + if (match) { + // This is greentext - wrap in span with greentext class + // Use > character (not >) since we're inserting HTML + const greentextContent = escapeHtml(match[2]); + return `>${greentextContent}`; + } + + return line; + }); + + return processedLines.join('\n'); + } + // Process content: replace nostr URIs with HTML span elements and convert media URLs function processContent(text: string): string { - // First, replace emoji shortcodes with images if resolved - let processed = replaceEmojis(text); + // First, convert greentext (must be before markdown processing) + let processed = convertGreentext(text); + + // Then, replace emoji shortcodes with images if resolved + processed = replaceEmojis(processed); // Convert hashtags to links processed = convertHashtags(processed); @@ -368,6 +396,20 @@ } }); + // Post-process HTML to convert blockquotes that are actually greentext + function postProcessGreentext(html: string): string { + // Find blockquotes that match greentext pattern (>text with no space) + // These are blockquotes that markdown created from greentext lines + // Pattern:

>text

where there's no space after > + const greentextBlockquotePattern = /]*>\s*]*>>([^\s<].*?)<\/p>\s*<\/blockquote>/g; + + return html.replace(greentextBlockquotePattern, (match, content) => { + // Convert to greentext span + const escapedContent = escapeHtml(content); + return `>${escapedContent}`; + }); + } + // Render markdown or AsciiDoc to HTML function renderMarkdown(text: string): string { if (!content) return ''; @@ -394,6 +436,9 @@ } } + // Post-process to fix any greentext that markdown converted to blockquotes + html = postProcessGreentext(html); + // Sanitize HTML (but preserve our data attributes and image src) const sanitized = sanitizeMarkdown(html); @@ -691,6 +736,25 @@ color: var(--fog-dark-text-light, #9ca3af); } + /* Greentext styling - 4chan style */ + :global(.markdown-content .greentext) { + color: #789922; + display: block; + margin: 0.25rem 0; + font-family: inherit; + } + + :global(.dark .markdown-content .greentext) { + color: #8fbc8f; + } + + /* Ensure greentext lines appear on their own line even if markdown processes them */ + :global(.markdown-content p .greentext), + :global(.markdown-content .greentext) { + display: block; + margin: 0.25rem 0; + } + /* Profile badges in markdown content should align with text baseline */ :global(.markdown-content [data-nostr-profile]), :global(.markdown-content .profile-badge) { diff --git a/src/lib/components/layout/Header.svelte b/src/lib/components/layout/Header.svelte index 2e18d0c..45b1a62 100644 --- a/src/lib/components/layout/Header.svelte +++ b/src/lib/components/layout/Header.svelte @@ -34,14 +34,20 @@