+ {/* Sticky header — matches letters/compose pattern */}
+
+
+
+
+ setActiveTab('write')}
+ >
+
+
+
+ setActiveTab('details')}
+ >
+
+
+
+ setActiveTab('drafts')}
+ >
+
+
+
+
+
+
+
+ Save Draft (Ctrl+S)
+
+
+
+
+
+
+
+ {/* ── Write tab ────────────────────────────────────────────── */}
+ {activeTab === 'write' && (
+
+ {/* Title Input */}
+
updateArticle('title', e.target.value)}
+ placeholder="Your article title..."
+ className="w-full text-3xl sm:text-4xl font-bold bg-transparent border-none outline-none placeholder:text-muted-foreground/40 mb-4"
+ />
+
+ {/* Editor */}
+
updateArticle('content', value || '')}
+ onUploadImage={handleImageUpload}
+ onImageButtonClick={handleInlineImageButtonClick}
+ placeholder="Start writing your article..."
+ className="rounded-xl overflow-hidden border border-border bg-card min-h-[400px]"
+ />
+
+ {/* Stats Bar */}
+
+ {wordCount} words
+ ·
+ {charCount} chars
+ ·
+ {readingTime} min read
+ {statusLabel && (
+ <>
+ ·
+ {statusLabel}
+ >
+ )}
+
+
+ )}
+
+ {/* ── Details tab ──────────────────────────────────────────── */}
+ {activeTab === 'details' && (
+
+ {/* Header Image */}
+
+
+ {article.image ? (
+
+

+
+
+
+
+ ) : (
+
+ )}
+
+
+ {/* URL Slug */}
+
+
+ updateArticle('slug', e.target.value)}
+ placeholder="article-url-slug"
+ className="font-mono text-sm"
+ />
+
+
+ {/* Tags */}
+
+
+
+ setTagInput(e.target.value)}
+ onKeyDown={(e) =>
+ e.key === 'Enter' && (e.preventDefault(), handleAddTag())
+ }
+ placeholder="Add a tag..."
+ className="flex-1"
+ />
+
+
+ {article.tags.length > 0 && (
+
+ {article.tags.map((tag) => (
+
+ #{tag}
+
+
+ ))}
+
+ )}
+
+
+ {/* Summary */}
+
+
+
+
+ )}
+
+ {/* ── Drafts tab ───────────────────────────────────────────── */}
+ {activeTab === 'drafts' && (
+
+ {isDraftsLoading && user ? (
+
+ ) : totalDrafts === 0 && publishedArticles.length === 0 ? (
+
+
+
+
+
No drafts or articles yet
+
+ Save a draft or publish to see content here
+
+
+ ) : (
+
+ {/* Drafts section */}
+ {totalDrafts > 0 && (
+
+
Drafts ({totalDrafts})
+ {combinedDrafts.map((draft) => (
+
handleLoadDraft(draft)}
+ >
+
+
+
+ {draft.title || 'Untitled Draft'}
+
+ {draft.summary && (
+
+ {draft.summary}
+
+ )}
+
+
+
+
+ {draft.isLocal ? (
+
+ ) : (
+
+ )}
+
+ {formatDistanceToNow(draft.updatedAt, { addSuffix: true })}
+ {draft.tags.length > 0 && (
+ <>
+ ·
+ {draft.tags.length} tags
+ >
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Published articles section */}
+ {publishedArticles.length > 0 && (
+
+
Published ({publishedArticles.length})
+ {publishedArticles.map((pub) => (
+
handleLoadArticle(pub)}
+ >
+
+
+
+ {pub.title || 'Untitled Article'}
+
+ {pub.summary && (
+
+ {pub.summary}
+
+ )}
+
+
+
+
+
+ Published {formatDistanceToNow(pub.publishedAt, { addSuffix: true })}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ )}
+
+ {/* Publish FAB — mobile: fixed bottom right */}
+
+
+ :
+ }
+ />
+
+ {/* Publish FAB — desktop: sticky within column */}
+
+
+ {/* Leave Confirmation Dialog */}
+
+
+
+ Unsaved changes
+
+ You have unsaved changes. Would you like to save your draft before
+ leaving?
+
+
+
+ setShowLeaveDialog(false)}>
+ Cancel
+
+
+
+ Save Draft
+
+
+
+
+
+ {/* Delete Draft Confirmation Dialog */}
+
setDeleteTarget(null)}>
+
+
+ Delete draft?
+
+ {deleteTarget?.isLocal
+ ? 'This draft will be permanently deleted from your browser.'
+ : 'This draft will be deleted from Nostr relays.'}
+
+
+
+ Cancel
+
+ {isDeleting ? (
+ <>
+
+ Deleting...
+ >
+ ) : (
+ 'Delete'
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/articles/LinkDialog.tsx b/src/components/articles/LinkDialog.tsx
new file mode 100644
index 00000000..c833228d
--- /dev/null
+++ b/src/components/articles/LinkDialog.tsx
@@ -0,0 +1,100 @@
+import { useState, useEffect } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+
+interface LinkDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ selectedText?: string;
+ onSubmit: (text: string, url: string) => void;
+}
+
+export function LinkDialog({ open, onOpenChange, selectedText, onSubmit }: LinkDialogProps) {
+ const [text, setText] = useState('');
+ const [url, setUrl] = useState('');
+
+ // Reset form when dialog opens
+ useEffect(() => {
+ if (open) {
+ setText(selectedText || '');
+ setUrl('');
+ }
+ }, [open, selectedText]);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (url.trim()) {
+ const finalText = text.trim() || url.trim();
+ let finalUrl = url.trim();
+
+ // Add https:// if no protocol specified
+ if (!/^https?:\/\//i.test(finalUrl)) {
+ finalUrl = 'https://' + finalUrl;
+ }
+
+ onSubmit(finalText, finalUrl);
+ onOpenChange(false);
+ }
+ };
+
+ const hasSelectedText = !!selectedText;
+
+ return (
+