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 @@