* refactor(editor): replace DOM manipulation with React node views and floating-ui
- Convert all inline node views (emoji, blob attachment, event preview) from
imperative document.createElement() to React components via ReactNodeViewRenderer
- Replace tippy.js with @floating-ui/react-dom for suggestion popup positioning
- Create useSuggestionRenderer hook bridging Tiptap suggestion callbacks to React state
- Extract shared EmojiMention, SubmitShortcut, and inline node extensions to separate files
- Extract types (EmojiTag, BlobAttachment, SerializedContent) to editor/types.ts
- Extract serialization logic to editor/utils/serialize.ts
- Remove redundant DOM keydown listener from RichEditor (handled by SubmitShortcut extension)
- Remove tippy.js dependency (-1045 lines net, RichEditor 632→297, MentionEditor 1038→354)
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix(editor): fix suggestion popover positioning, scrolling, and profile click behavior
- Replace UserName component in ProfileSuggestionList with plain text display
so clicking a suggestion autocompletes instead of opening their profile
(UserName has an onClick that calls addWindow and stopPropagation)
- Add react-virtuoso to ProfileSuggestionList for efficient lazy rendering
of up to 20 search results with fixed item height scrolling
- Add profile avatars with lazy loading and initial-letter fallback
- Fix SuggestionPopover positioning with autoUpdate for scroll/resize tracking
- Add size middleware to constrain popover max-height to available viewport space
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* refactor(editor): convert emoji suggestion from grid to scrollable list with Virtuoso
Replace the 8-column grid layout with a vertical list matching the profile
suggestion style — each row shows the emoji preview alongside its :shortcode:
name. Uses react-virtuoso with fixedItemHeight for lazy rendering and smooth
keyboard-driven scrolling through large emoji sets.
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix(editor): set mentionSuggestionChar to ':' for emoji nodes
When backspacing over a mention-based node, Tiptap inserts the node's
mentionSuggestionChar attribute as undo text. The EmojiMention extension
inherits Mention's default of '@', so deleting an emoji left '@' instead
of ':'. Fix by explicitly setting mentionSuggestionChar: ':' in the emoji
command's attrs for both RichEditor and MentionEditor.
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* test(editor): add comprehensive test suite for custom TipTap extensions
Tests all 8 custom extensions using headless TipTap Editor instances in
jsdom environment (TipTap has no official testing package):
- EmojiMention: schema, renderText (unicode vs custom), mentionSuggestionChar
attribute handling, backspace behavior regression test
- BlobAttachmentRichNode/InlineNode: schema (block vs inline), attributes,
renderText URL serialization, parseHTML selectors
- NostrEventPreviewRichNode/InlineNode: schema, renderText encoding for
note/nevent/naddr back to nostr: URIs
- SubmitShortcut: Mod-Enter always submits, Enter behavior with
enterSubmits flag
- FilePasteHandler: media type filtering (image/video/audio), non-media
rejection, mixed paste filtering, edge cases (no files, no callback)
- NostrPasteHandler: bech32 regex matching (npub/note/nevent/naddr/nprofile),
nostr: prefix handling, URL exclusion, node creation (mention vs preview),
surrounding text preservation, multiple entities
- Serialization: formatBlobSize, serializeRichContent (emoji tag extraction,
blob dedup, address refs), serializeInlineContent (mention→nostr: URI,
emoji→shortcode, blob→URL, event preview encoding)
90 new tests total.
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix(editor): paste handler and serialization bugs found via adversarial testing
NostrPasteHandler fixes:
- Punctuation after bech32 now matches (npub1..., npub1...! etc.)
Changed trailing lookahead from (?=$|\s) to (?=$|\s|[.,!?;:)\]}>])
- Fixed double-space between entities — unconditional " " after every
entity caused doubled spaces. Now only adds trailing space when entity
is at the very end of pasted text (for cursor positioning).
- Tightened regex character class from [\w] to [a-z0-9] to match actual
bech32 charset (rejects uppercase, underscore)
- Wrapped dispatch in try/catch to handle block-node-at-inline-position
errors gracefully (falls back to default paste)
Serialization fix:
- serializeRichContent now guards blob collection with `url && sha256`
matching the defensive checks already in serializeInlineContent.
Previously null sha256 would corrupt the dedup Set and null url would
produce invalid BlobAttachment entries.
Added 22 new edge case tests:
- Paste handler: punctuation boundaries, double-space regression,
malformed bech32 fallback, uppercase rejection, error resilience
- Serialization: empty editor, null sha256/url blobs, invalid pubkey
fallback, missing mention attrs, inline dedup, multi-paragraph
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix(editor): raise suggestion search limits for profiles and emojis
Both suggestion dropdowns use Virtuoso for virtualized rendering, so
they can handle large result sets without performance issues. The
previous limits (20 profiles, 24 emojis) were too restrictive — users
with many custom emojis sharing a substring or large contact lists
couldn't scroll to find the right match.
Raised both limits to 200 to allow thorough browsing while still
bounding the result set.
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* refactor(chat): rework emoji picker to scrollable list with search
Replace the fixed 1-row grid (8 emojis) with a scrollable virtualized
list matching the editor's EmojiSuggestionList look & feel:
- Search box at top with magnifying glass icon
- Virtuoso-backed scrollable list (8 visible items, unlimited results)
- Each row shows emoji icon + :shortcode: label
- Keyboard navigation: arrow keys to select, Enter to confirm
- Mouse hover highlights, click selects
- Frequently used emojis still shown first when no search query
- Narrower dialog (max-w-xs) for a compact picker feel
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix: add address field to EmojiTag in editor types, fix GroupMessageOptions
- Add optional `address` field to EmojiTag in editor/types.ts to match
NIP-30 changes from main (30030 emoji set address)
- Extend GroupMessageOptions with MetaTagOptions to fix type error in
GroupMessageBlueprint's setMetaTags call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): restore address attr, fix serialization, UserName, no-scroll
- Restore `address` attribute in shared EmojiMention extension (emoji.ts)
that was dropped during refactor — required for NIP-30 emoji set tracking
- Extract `address` from emoji nodes in both serializeRichContent and
serializeInlineContent so it makes it into published events
- Fix MentionEditorProps.onSubmit signature: use EmojiTag[] (not the narrower
inline type) so address field flows through to callers
- Restore UserName component in ProfileSuggestionList for proper display
with Grimoire member badges and supporter flame
- Remove scrollbar when all items fit: set overflow:hidden on Virtuoso when
items.length <= MAX_VISIBLE (profile list, emoji list, emoji picker dialog)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor: extract relay auth manager into standalone package
Decouple NIP-42 relay authentication from Grimoire internals into a
generic, framework-agnostic package at packages/relay-auth-manager/.
The new package uses dependency injection for pool, signer, and storage
(localStorage-like interface), making it reusable in any applesauce-based
app. Fixes the bug where auth prompts appeared even when the signer
couldn't sign events - now only emits pending challenges when a signer
is available.
Key changes:
- New package with RelayAuthManager class, pure auth state machine,
and comprehensive test suite (103 tests)
- Grimoire's relay-state-manager now delegates all auth logic to the
package, retaining only connection/notice tracking
- Auth preferences moved from Dexie to localStorage via pluggable storage
- Reactive signer lifecycle: auto-auth re-evaluates when signer appears
- Pool relay lifecycle via add$/remove$ observables (no polling)
https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd
* fix: prevent auth prompts when signer is unavailable
Three bugs fixed:
1. Race condition in emitState(): states$ was emitted before
pendingChallenges$, so relay-state-manager's states$ subscriber
would read stale pendingChallenges$.value. Now pendingChallenges$
is emitted first for consistent reads.
2. relay-state-manager only subscribed to states$, missing
pendingChallenges$ changes. Now subscribes to both.
3. canAccountSign used constructor.name which is fragile under
minification. Now uses account.type !== "readonly" (stable
property from applesauce-accounts).
Added 3 regression tests verifying pendingChallenges$.value
consistency when observed from states$ subscribers.
https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd
* fix: migrate auth preferences from Dexie to localStorage
One-time migration preserves existing user preferences: reads from
Dexie relayAuthPreferences table, writes to localStorage, injects
into the running manager, then clears the Dexie table. Skips if
localStorage already has data (idempotent).
Removes the DexieAuthStorage adapter — sync/async impedance mismatch
made it fragile. localStorage is the right fit for the sync
getItem/setItem interface.
https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd
* docs: add README for relay-auth-manager package
Covers constructor options, observables, methods, auth lifecycle,
preferences, storage interface, state machine, and dependency
interfaces.
https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd
* fix: address 3 bugs in relay-auth-manager
1. State machine: authenticated + CHALLENGE_RECEIVED with "never"
preference now correctly auto-rejects instead of entering a dead
challenge_received state that nobody acts on.
2. Auto-auth error handlers now guard with `status === "authenticating"`
before overwriting state, preventing a late-resolving promise from
clobbering a valid state transition (e.g. disconnect → "none").
3. API no longer leaks mutable internal references. getRelayState(),
getAllStates(), getAllPreferences(), and states$ emissions all return
shallow copies. Previously, consumers holding a reference to a
previous emission would see it silently mutate.
All three fixes include regression tests (110 tests, up from 103).
https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd
* feat: make relay-auth-manager production-ready
- authenticate() now waits for authenticated$ confirmation via
firstValueFrom(race(...)) — resolves only when relay confirms,
rejects on disconnect. Uses Promise.all to avoid unhandled rejections.
- Add retry() for relays stuck in "failed" state.
- Add removePreference() to prevent unbounded preference growth.
- Add normalizeUrl option for custom URL normalization — preferences and
session rejections now consistently normalized via normalizeForKey().
- Wire Grimoire's normalizeRelayURL as the normalizer in relay-auth.ts.
- Update README with new methods, normalizeUrl option, retry lifecycle.
- 123 package tests passing.
https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd
* chore: prepare relay-auth-manager for npm publishing
- Point exports/main/types to dist/ for correct npm resolution
- Add tsconfig.build.json for emitting ESM JS + declarations
- Add build/clean/prepublishOnly scripts
- Add LICENSE, description, keywords, author, files field
- Add Vite resolve alias + tsconfig paths for workspace dev
(resolves source directly, no pre-build needed for dev)
- Fix TypeScript strict errors in test file
- Clean npm pack output: dist/, README.md, LICENSE only
https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add plan for repository tree visualization feature
Comprehensive plan covering:
- git-natural-api library analysis and API documentation
- useGitTree/useGitBlob hooks for fetching git data
- FileTreeView component using Radix Collapsible
- Shiki migration with on-demand language loading
- Multi-server fallback for redundant clone URLs
- Dexie caching for offline access
* docs: add comprehensive Shiki migration plan
Detailed plan for migrating from Prism.js to Shiki with lazy loading:
- Analysis of all 8 components using SyntaxHighlight
- Shiki service with singleton highlighter and on-demand language loading
- Custom Grimoire dark theme matching current Prism styles
- Language alias mapping for 50+ extensions
- React hook for async highlighting with loading states
- CSS theme preserving minimalistic dark aesthetics
- Migration steps with rollback plan
* feat: migrate syntax highlighting from Prism.js to Shiki
Replace Prism.js with Shiki for syntax highlighting with several key improvements:
- Lazy loading: Languages loaded on-demand via dynamic imports instead of bundling all upfront
- Broader language support: 200+ TextMate grammars vs 11 statically imported
- Singleton highlighter: Core languages (JS, TS, JSON, diff, bash) preloaded, others loaded on first use
New files:
- src/lib/shiki.ts: Shiki service with highlightCode(), normalizeLanguage(), language aliases
- src/hooks/useHighlightedCode.ts: React hook for async highlighting with loading states
- src/styles/shiki-theme.css: Grimoire dark theme matching previous minimalistic style
Updated components:
- SyntaxHighlight: Now uses Shiki with graceful loading/error states
- CodeSnippetRenderer/DetailRenderer: Simplified, removed manual language mapping
- MarkdownContent: Removed type casts, any language now supported
Removed:
- prismjs and @types/prismjs dependencies
- src/styles/prism-theme.css
* feat: add repository file tree visualization
Add file tree explorer to the Repository detail renderer (kind 30617)
using @fiatjaf/git-natural-api for fetching git trees via HTTP.
New files:
- src/lib/git-types.ts: TypeScript types for DirectoryTree, SelectedFile, etc.
- src/hooks/useGitTree.ts: Hook to fetch git repository tree from clone URLs
- Tries multiple clone URLs in sequence
- Uses getDirectoryTreeAt with filter capability when available
- Falls back to shallowCloneRepositoryAt otherwise
- src/hooks/useGitBlob.ts: Hook to fetch individual file content by hash
- Detects binary files
- Returns both raw Uint8Array and decoded text
- src/components/ui/FileTreeView.tsx: Recursive tree view component
- Collapsible directories with chevron icons
- File icons based on extension (code, json, text, image, etc.)
- Alphabetical sorting with directories first
- src/components/nostr/kinds/RepositoryFilesSection.tsx: Main integration
- Side-by-side tree and file preview layout
- Syntax-highlighted file content using existing SyntaxHighlight
- Binary file detection with appropriate UI
- Loading/error states
Modified:
- RepositoryDetailRenderer.tsx: Added RepositoryFilesSection below relays
Dependencies:
- Added @fiatjaf/git-natural-api from JSR
* fix: improve repository tree visualization UX
- Collapse directories by default in file tree
- Hide files section on tree loading error
- Add code-like skeleton loader with file header
- Fix syntax highlight size jump between loading/loaded states
- Replace purple accent with grayscale theme
- Preload Rust and Markdown languages for reliable highlighting
* refactor: improve git tree and syntax highlighting
- Remove shallow clone fallback from useGitTree, only use no-blobs fetch
Servers without filter capability are skipped instead of downloading blobs
- Add light theme support for Shiki syntax highlighting
Theme is automatically selected based on current color scheme
* fix: improve dark theme contrast for syntax highlighting
* fix: address code review issues
- useGitTree: use useStableArray for cloneUrls to fix dependency tracking
- useGitTree/useGitBlob: add isMounted checks to prevent state updates after unmount
- RepositoryFilesSection: remove unnecessary useMemo for language
- FileTreeView: use path instead of hash for React keys
- shiki: track failed languages to avoid repeated console warnings
* fix: improve dark theme contrast for syntax highlighting
- Add CSS variables for syntax highlighting instead of hardcoded colors
- Add --syntax-constant and --syntax-tag variables to light and dark themes
- Use high contrast colors for dark mode (bright green strings, purple keywords)
- Simplify Shiki transformer to output CSS classes instead of inline styles
- Remove unused parameters from transformer callback
* fix: restore syntax highlighting colors
Revert the CSS class-based approach which was failing to classify tokens.
Instead, keep Shiki's inline styles from the theme and only remove
backgrounds to let CSS handle those. The theme colors now provide
syntax highlighting directly.
* feat: add copy button and CSS variable-based syntax highlighting
- Add copy button next to file name in file viewer header (icon-only)
- Use Shiki's createCssVariablesTheme for proper theme integration
- Map Shiki CSS variables to our theme system variables
- Syntax highlighting now works correctly across all themes (light/dark)
* refactor: create IconCopyButton component and use CopyCheck consistently
- Add IconCopyButton component for reusable icon-only copy buttons
- Refactor RepositoryFilesSection to use IconCopyButton
- Replace Check with CopyCheck in ChatMessageContextMenu
- Replace Check with CopyCheck in BaseEventRenderer
- Use text-success instead of text-green-500 for consistency
* fix: add HTML, CSS, TOML to core languages and expand token mappings
- Add html, css, toml to CORE_LANGUAGES for eager loading
- Add variableDefaults to cssVarsTheme for proper initialization
- Expand shiki-theme.css with more token type mappings:
- HTML/XML: tag, attribute, attr-value
- CSS: selector, property
- Additional: variable, operator, number, boolean, regex, etc.
* fix: improve diff line spacing with flex layout
- Use flex-col with gap-0 on code element for tight line packing
- Reduce line-height from 1.5 to 1.4 for tighter spacing
- Add .line display:block with min-height for consistent sizing
- Simplify diff background styling (remove negative margin hack)
* fix: improve code block line spacing and wrap long lines
- Increase line-height from 1.4 to 1.5 for better readability
- Use pre-wrap instead of pre to allow long line wrapping
- Add overflow-wrap: break-word to break long URLs/strings
* chore: remove planning docs
* chore: update @fiatjaf/git-natural-api to 0.2.3
* fix: make code blocks horizontally scrollable with full-width diff backgrounds
- Use white-space: pre for horizontal scrolling instead of wrapping
- Add width: fit-content and min-width: 100% to code element
- Ensure diff line backgrounds extend full width when scrolling
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add Grimoire logo SVG and generate PWA assets
- Add logo.svg with the official Grimoire logo and gradient
- Create GrimoireLogo React component for use in the app
- Add scripts/generate-pwa-icons.mjs to generate all PWA icons from SVG
- Regenerate all favicon and PWA icons from the new logo
- Update mobile welcome screen to show the logo instead of text
* feat: use transparent backgrounds for PWA icons and add theme gradient option
- Update generate-pwa-icons.mjs to output PNGs with transparent backgrounds
- Add gradient prop to GrimoireLogo component ("original" or "theme")
- Theme gradient matches text-grimoire-gradient CSS (yellow -> orange -> purple -> cyan)
- Mobile welcome screen now uses theme gradient to match ASCII art
* feat: use original gradient for mobile welcome logo
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add client tag support to all event creation
Implements a global settings system to control whether the Grimoire client tag
should be included in all published events. This allows users to opt-in or
opt-out of identifying their client in published events.
Changes:
- Created global settings service (src/services/settings.ts) with reactive
BehaviorSubject for app-wide configuration
- Created useSettings hook (src/hooks/useSettings.ts) for React components
- Migrated PostViewer from local settings to global settings system
- Added client tag support to:
- Post publishing (PostViewer.tsx)
- Spell publishing (publish-spell.ts)
- Event deletion (delete-event.ts)
- NIP-29 chat messages, reactions, join/leave, and group bookmarks
(nip-29-adapter.ts)
- Zap requests (create-zap-request.ts)
The client tag setting defaults to enabled (true) for backward compatibility.
Users can toggle this in the post composer settings dropdown.
All event creation locations now check settingsManager.getSetting("includeClientTag")
before adding the GRIMOIRE_CLIENT_TAG to event tags.
* refactor: exclude client tags from NIP-29 and zap requests
Remove client tag support from NIP-29 adapter events and zap requests
as these events may be rejected by servers with large tags or have
specific size constraints.
Changes:
- Removed client tag from NIP-29 chat messages (kind 9)
- Removed client tag from NIP-29 reactions (kind 7)
- Removed client tag from NIP-29 join/leave requests (kind 9021, 9022)
- Removed client tag from NIP-29 group bookmarks (kind 10009)
- Removed client tag from zap requests (kind 9734)
Client tags remain enabled for:
- Regular posts (kind 1)
- Spell publishing (kind 777)
- Event deletion (kind 5)
This ensures maximum compatibility with relay servers and LNURL endpoints
while still providing client identification for standard events.
* feat: implement comprehensive namespaced settings system
Redesigned the settings system with proper namespacing, type safety, validation,
and migration support. This provides a solid foundation for all app configuration.
Settings Structure:
- post: Post composition settings (client tag, relay selection)
- appearance: UI/theme settings (theme, compact mode, font size, animations)
- relay: Relay configuration (fallback, discovery, outbox, timeouts)
- privacy: Privacy settings (read receipts, content warnings, link warnings)
- database: Caching settings (max events, cleanup, IndexedDB options)
- notifications: Browser notifications preferences
- developer: Debug and experimental features
Key Features:
- Fully typed with TypeScript interfaces for each namespace
- Automatic validation with fallback to defaults for invalid data
- Migration system from old flat structure to namespaced structure
- Backwards compatible with old "grimoire-settings" localStorage key
- Import/export functionality for settings backup/restore
- Reactive updates via RxJS BehaviorSubject
- Section-level and individual setting updates
- Reset individual sections or all settings
Changes:
- Created comprehensive AppSettings interface with 7 namespaced sections
- Implemented SettingsManager class with reactive updates and persistence
- Updated useSettings hook to support namespaced API
- Updated PostViewer, publish-spell, and delete-event to use new API
(settingsManager.getSetting("post", "includeClientTag"))
- Added extensive inline documentation for all settings
Migration:
- Automatically migrates old includeClientTag setting to post.includeClientTag
- Moves data from "grimoire-settings" to "grimoire-settings-v2" key
- Validates all loaded settings and fills in defaults for missing values
This foundation will support future settings UI with tabbed interface.
* feat: add comprehensive settings UI with Post and Appearance sections
Created a minimal MVP settings system accessible via command palette and user menu.
Settings are organized in a clean tabbed interface with two initial sections.
UI Features:
- SettingsViewer component with sidebar navigation
- Post section: Toggle to include Grimoire client tag in published events
- Appearance section:
- Theme selector (light/dark/system)
- Toggle to show/hide client tags in event UI ("via Grimoire" etc)
Integration:
- Added "settings" command to command palette
- Added "Settings" option to user menu (before Support Grimoire)
- Registered "settings" as new AppId in window system
Display Logic:
- BaseEventRenderer now honors settings.appearance.showClientTags
- When disabled, "via Grimoire" and other client tags are hidden from events
- Setting applies instantly across all event renderers
Technical Details:
- SettingsViewer uses existing UI components (Checkbox, Button, Label)
- Leverages useSettings hook for reactive updates
- Settings persist to localStorage via settingsManager
- Simple button group for theme selection instead of dropdown
- Clean two-column layout with icons for each section
This provides a solid foundation for adding more settings sections later
(relay config, privacy, database, notifications, developer options).
* refactor: redesign settings UI with shadcn Tabs and cleaner layout
Simplified the settings UI to be more minimal and follow Grimoire patterns:
Changes:
- Use shadcn Tabs component instead of custom sidebar navigation
- Remove non-working theme selector (no theme system implemented yet)
- Use regular <label> elements with htmlFor instead of custom Label component
- Shorter, cleaner copy throughout
- Properly remove unused theme-related imports from user menu
- Follow Grimoire UI patterns (similar to SettingsDialog)
Settings sections:
- Post: Include client tag setting
- Appearance: Show client tags setting
The UI is now cleaner, uses proper form semantics, and matches the rest
of Grimoire's design system.
* feat: add working theme selector and improve settings UI
Added fully functional theme selector and improved the settings UI with
better components and cleaner copy.
Changes:
- Created Switch component (shadcn/radix-ui) for boolean settings
- Added working theme selector that integrates with existing theme system
- Uses useTheme hook to display available themes (Dark, Light, Plan9)
- Reordered sections: Appearance first, Post second
- Reordered settings: Theme first in Appearance section
- Replaced Checkbox with Switch for better UX on boolean toggles
- Simplified copy: "Add Grimoire tag to published events" instead of listing kinds
- Simplified copy: "Display client identifiers in events" instead of "via Grimoire" mention
- Better layout: Label/description on left, Switch on right
Settings now use proper form components:
- Switch for boolean toggles (include client tag, show client tags)
- Button group for theme selection
- Clean justify-between layout for settings rows
The theme selector works immediately - clicking Dark/Light/Plan9 applies
the theme instantly via the existing ThemeProvider context.
* refactor: improve settings UI with Select, icons, and better spacing
Enhanced the settings UI with shadcn Select component, tab icons, and
improved typography and spacing.
Changes:
- Created Select component (shadcn/radix-ui) for dropdowns
- Added icons to tabs: Palette for Appearance, FileEdit for Post
- Replaced button group theme selector with Select dropdown (w-48)
- Added gap-4 between labels and switches for better spacing
- Increased setting names from text-sm to text-base (bigger)
- Reduced help text from text-sm to text-xs (smaller)
- Added gap-3 between "Theme" label and Select
- Added font-medium to "Theme" label for consistency
Layout improvements:
- Theme selector now uses Select with 192px width
- All setting rows have gap-4 between content and controls
- Consistent text hierarchy: font-medium for labels, text-xs for descriptions
- Tab triggers have gap-2 between icon and text
The Select component is properly integrated with the theme system and
displays Dark, Light, and Plan9 options with checkmark indicators.
* refactor: use horizontal layout for theme selector
Aligned theme selector with other settings by using horizontal layout:
- Label and description on left, Select on right
- Added gap-4 for consistent spacing
- Changed label from text-sm to text-base font-medium
- Added helpful description: 'Choose your color scheme'
- Added id='theme' for accessibility
Now all settings follow the same visual pattern.
* refactor: reduce theme selector width for compact display
Changed theme selector from w-48 (192px) to w-32 (128px) since
theme names are short (Dark, Light, Plan9). More compact and
better proportioned for the content.
* fix: pass 'settings' command string when opening from menu
Changed the command string from 'Settings' to 'settings' when opening
the settings window from the user menu. This ensures clicking edit on
the settings window shows the correct command: 'settings'
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add Nostr Wallet Connect (NWC) integration
Add NWC (NIP-47) support to connect Lightning wallets:
- Add NWCConnection type and state management
- Implement custom NWC client service for wallet communication
- Create ConnectWalletDialog for entering connection strings
- Add wallet button to user menu showing balance
- Display wallet info (balance, alias) in user menu dropdown
- Support get_info, get_balance, pay_invoice, make_invoice commands
- Persist wallet connection to localStorage
- Install applesauce-wallet (for future NIP-60 support)
Note: Current implementation uses custom NWC client. Can optionally
switch to @getalby/sdk for official Alby SDK support.
* refactor: use applesauce-wallet-connect and move wallet to header
Replace custom NWC client implementation with applesauce-wallet-connect:
- Install applesauce-wallet-connect for official NIP-47 support
- Create nwc.ts service wrapper for WalletConnect singleton
- Update NWCConnection type to match WalletConnectURI interface
- Use service/relays/secret properties instead of custom names
Move wallet display from user menu to header:
- Create standalone WalletButton component
- Add WalletButton to header next to UserMenu
- Remove wallet UI from user menu dropdown
- Show balance in header with yellow zap icon
- Clicking wallet button opens connect dialog
This provides better UX with wallet status visible in header
and uses the official applesauce implementation for reliability.
* feat: improve wallet button UX and add detailed info dialog
UI improvements:
- Remove border and padding from wallet button (use ghost variant)
- Remove "sats" suffix from balance display (show just the number)
- Change click behavior to show detailed wallet info dialog
Wallet info dialog:
- Show balance prominently without suffix
- Display wallet alias if available
- Show lightning address (lud16) if present
- List all supported NWC methods as badges
- Display connected relay URLs
- Add disconnect button with confirmation toast
This provides a cleaner header appearance and better wallet
management with all details accessible in one place.
* refactor: move wallet functionality to user menu
Move wallet connection and info from separate header button into
the user menu dropdown for better organization.
Changes:
- Remove standalone WalletButton component
- Add wallet section to user menu dropdown
- Show "Connect Wallet" option when no wallet is connected
- Display wallet balance and alias when connected
- Clicking wallet info opens detailed dialog with:
- Balance (without suffix)
- Wallet name/alias
- Lightning address (lud16)
- Supported NWC methods
- Connected relay URLs
- Disconnect button
This consolidates all user-related settings (account, relays,
blossom servers, wallet) in one consistent location.
* feat: improve wallet UX with profile-based naming and better layout
UX improvements:
- Use service pubkey username/profile as wallet name instead of alias
- Remove format hint text from connection string input
- Move wallet to single row directly below user name (remove label)
- Use RelayLink component for relay URLs in wallet info dialog
- Show wallet name from service profile with fallback to alias/pubkey
This provides better integration with Nostr profiles and a cleaner,
more compact menu layout. The wallet service provider's identity is
now shown using their actual Nostr profile name.
* feat: add persistent wallet service with auto-updating balance
Implement comprehensive NWC wallet management architecture:
**Service Layer** (`src/services/nwc.ts`):
- Maintain singleton WalletConnect instance across app lifetime
- Poll balance every 30 seconds for automatic updates
- Subscribe to NIP-47 notifications (kind 23197) for real-time updates
- Expose RxJS observable (balance$) for reactive balance changes
- Restore wallet from saved connection on app startup
- Proper cleanup on disconnect
**Hook Layer** (`src/hooks/useWallet.ts`):
- useWallet() hook exposes wallet throughout the app
- Provides methods: payInvoice, makeInvoice, getBalance, etc.
- Auto-syncs balance updates with Jotai state
- Handles wallet restoration on mount
- Type-safe wrapper around WalletConnect API
**UI Updates**:
- Add refresh button to wallet info dialog
- Auto-update balance display when transactions occur
- Proper cleanup on wallet disconnect
**Architecture**:
```
User Action → useWallet() → NWC Service → WalletConnect
↓ ↓
Update State ← balance$ observable ← Polling/Notifications
```
This enables:
- Real-time balance updates when paying/receiving
- Easy wallet access: `const { payInvoice, balance } = useWallet()`
- Persistent connection across page reloads
- Automatic polling as fallback for notifications
* refactor: make NWC fully reactive with notifications and graceful balance handling
Changes:
- Remove polling mechanism in favor of pure reactive notifications$ observable
- Subscribe to wallet.notifications$ for real-time balance updates
- Make balance display conditional (only show if available)
- Fix TypeScript errors (notification.type access, unused variable)
- Remove Jotai callback mechanism for balance updates
- Use use$() directly for reactive balance subscription
- Update comments to reflect reactive architecture (no polling)
The wallet now updates balance automatically when payments are sent/received
via NIP-47 notifications, with no polling overhead.
* feat: improve wallet UX with profile-based naming and better layout
Improvements to NWC wallet UI:
- Add separator between user info and wallet section in menu
- Show wallet icon instead of zap icon for better clarity
- Display connection status indicator (green/red dot) in both menu and dialog
- Make wallet service username clickable in wallet info dialog to open profile
- Use wallet relays as hints when fetching service profile for better resolution
- Enhanced useProfile hook to accept optional relay hints parameter
The wallet now properly resolves service profiles using the NWC relay
and shows visual connection status at a glance.
* fix: remove toast descriptions for better contrast
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Add NIP-58 Badge Definition renderers
Implement feed and detail renderers for kind 30009 Badge Definition events.
- Add nip58-helpers.ts with badge metadata extraction functions
- Create BadgeDefinitionRenderer for compact feed view
- Create BadgeDefinitionDetailRenderer with award statistics
- Register both renderers in kinds registry
Badge definitions display:
- Badge image or Award icon fallback
- Badge name, description, and identifier
- In detail view: issuer, award count, recipients, image variants
- Automatically queries for badge awards (kind 8) to show stats
Follows existing renderer patterns (ZapstoreApp, EmojiSet) with
reactive queries using useLiveTimeline and cached helpers.
* refactor: Simplify badge renderers
Simplify NIP-58 badge renderers based on feedback:
- Rename "badge definition" to "badge" in comments and docs
- Remove image and ID from feed view (show only name + description)
- Remove award statistics fetching/display from detail view
- Remove badge address section from detail view
Feed view now shows minimal info (name, description) while detail
view focuses on badge metadata and image variants without external queries.
* feat: Add NIP-58 Badge Award renderers (kind 8)
Implement feed and detail renderers for Badge Award events that:
- Fetch and display badge metadata (thumbnail, name)
- Show recipient count in feed view ("Awarded to n people")
- Display full recipient list with usernames in detail view
- Link badge thumbnail and name to the badge event (kind 30009)
- Support award comments in detail view
Technical details:
- Parse badge address from "a" tag (30009:pubkey:identifier format)
- Fetch badge event reactively using eventStore.replaceable
- Use AddressPointer from nostr-tools/nip19 for type safety
- Register kind 8 in both feed and detail renderer registries
Also updates all "badge definition" terminology to just "badge"
in code comments for consistency with user-facing strings.
* refactor: Make badge award feed view more compact
Change BadgeAwardRenderer to display inline compact format:
- Small thumbnail (size-6) inline with text
- Badge name linked to badge event
- "awarded to n people" linked to award event (clickable)
- All elements in single line with flex-wrap for overflow
Layout: [thumbnail] badge-name awarded to n people
* feat: Show username when badge awarded to single person
Update BadgeAwardRenderer to display the recipient's username
when only 1 person is awarded the badge, instead of "1 person".
- Single recipient: "awarded to @username"
- Multiple recipients: "awarded to n people"
Uses UserName component for proper profile name resolution.
* feat: Add NIP-58 Profile Badges renderer (kind 30008)
- Add ProfileBadgesRenderer for feed view showing first 4 badges with count
- Add ProfileBadgesDetailRenderer for detail view showing all badges in grid
- Add getProfileBadgePairs helper to extract badge pairs from events
- Adjust BadgeAwardRenderer icon size from 6 to 5 and spacing to gap-1.5
- Register kind 30008 in both feed and detail renderer registries
Completes NIP-58 implementation with all three event types:
- Kind 8: Badge Awards
- Kind 30009: Badge Definitions
- Kind 30008: Profile Badges (this commit)
* refactor: Improve Profile Badges UX
Feed view:
- Show all badge thumbnails (removed 4-badge limit)
- Entire feed item is clickable to open detail view
- Badge count displayed inline
Detail view:
- Change from grid to vertical list layout
- Show one badge per row with horizontal layout
- Display: awarded by author, badge image, name, and description
- Better readability for badge information
* refactor: Improve Profile Badges layout and hierarchy
Feed view:
- Badge count now appears as clickable title
- Thumbnails displayed below title in separate row
- Better visual hierarchy and clearer affordance
Detail view:
- Increase badge images from size-16 to size-24
- Remove "Awarded by" label, show issuer directly
- Cleaner, more prominent badge presentation
* feat: Add Badge Awards (kind 8) to chat as system messages
Implemented NIP-58 badge award rendering in chat adapters:
Chat types (src/types/chat.ts):
- Add kind 8 to CHAT_KINDS array
- Add badgeAddress and awardedPubkeys to MessageMetadata
NIP-29 adapter (src/lib/chat/adapters/nip-29-adapter.ts):
- Include kind 8 in message filters
- Convert badge awards to system messages
- Extract badge metadata (address, recipients)
ChatViewer (src/components/ChatViewer.tsx):
- Add BadgeAwardSystemMessage component
- Parse badge address and fetch badge definition
- Render: "* username awarded 🏅 badge-name to username(s)"
- Show badge icon/image inline with badge name
Badge awards now appear as system messages showing issuer, badge
icon, badge name, and recipients in a clean horizontal layout.
* feat: Add Badge Awards (kind 8) to NIP-53 live chat
Extended badge award system messages to NIP-53 live streaming chats:
NIP-53 adapter (src/lib/chat/adapters/nip-53-adapter.ts):
- Import getAwardedPubkeys and getTagValues helpers
- Add kind 8 to message filters (loadMessages and loadMoreMessages)
- Add badge award handler in eventToMessage
- Convert to system messages with badge metadata
Badge awards from stream hosts now appear in live chat as system
messages, showing issuer, badge icon, and recipients in real-time.
* Revert "feat: Add Badge Awards to chat"
This reverts commits:
- 1686a94 feat: Add Badge Awards (kind 8) to NIP-53 live chat
- 909359f feat: Add Badge Awards (kind 8) to chat as system messages
Badge award rendering in chat will be implemented later.
This keeps the PR focused on Profile Badges (kind 30008) implementation.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add right-click/long-press context menu to chat messages with event interactions:
- Reply to message
- Copy message text
- Open event detail (opens in new window)
- Copy event ID (nevent/naddr with relay hints)
- View raw JSON
Implements the same EventMenu pattern used in feed renderers, providing
consistent UX across the app. No visible UI elements added - menu appears
only on right-click or long-press (mobile).
Components added:
- src/components/ui/context-menu.tsx - Radix UI context menu primitives
- src/components/chat/ChatMessageContextMenu.tsx - Chat-specific context menu
- Integrated into MessageItem in ChatViewer.tsx
Dependencies:
- Added @radix-ui/react-context-menu
Co-authored-by: Claude <noreply@anthropic.com>
* Add Blossom blob storage integration
- Add blossom-client-sdk dependency for blob storage operations
- Create blossom.ts service with upload, list, check, mirror, delete primitives
- Add kind 10063 server list fetching and parsing
- Create blossom-parser.ts for command argument parsing with subcommands
- Add BLOSSOM command to man.ts with subcommands:
- servers: Show configured Blossom servers
- check: Check server health
- upload: Upload files to user's servers
- list: List blobs for a user
- mirror: Mirror blobs between servers
- delete: Delete blobs from servers
- Create BlossomViewer component with views for each subcommand
- Wire up BlossomViewer in WindowRenderer
- Add Blossom servers dropdown to ProfileViewer header
- Upload primitives can be called programmatically for use in other components
* Enhance Blossom viewer with server selection and blob details
- Add server selection checkboxes to upload view for choosing target servers
- Add BlobDetailView with media preview (image/video/audio) and metadata display
- Add 'blob' subcommand to view individual blob details
- Remove unused 'check' subcommand
* Add Blossom upload dialog with chat integration
- Create BlossomUploadDialog component with file picker, server selection, and preview
- Create useBlossomUpload hook for easy integration in any component
- Add insertText method to MentionEditor for programmatic text insertion
- Integrate upload button (paperclip icon) in chat composer
- Supports image, video, and audio uploads with drag-and-drop
* Add rich blob attachments with imeta tags for chat
- Add BlobAttachment TipTap extension with inline preview (thumbnail for images, icons for video/audio)
- Store full blob metadata (sha256, url, mimeType, size, server) in editor nodes
- Convert blob nodes to URLs in content with NIP-92 imeta tags when sending
- Add insertBlob method to MentionEditor for programmatic blob insertion
- Update NIP-29 and NIP-53 adapters to include imeta tags with blob metadata
- Pass blob attachments through entire send flow (editor -> ChatViewer -> adapter)
* Add fallback public Blossom servers for users without server list
- Add well-known public servers as fallbacks (blossom.primal.net, nostr.download, files.v0l.io)
- Use fallbacks when user has no kind 10063 server list configured
- Show "Public Servers" label with Globe icon when using fallbacks
- Inform user that no server list was found
- Select first fallback server by default (vs all user servers)
* Fix: Don't show fallback servers when not logged in
Blossom uploads require signed auth events, so users must be logged in.
The 'Account required' message is already shown in this case.
* Remove files.v0l.io from fallback servers
* Add rich renderer for kind 10063 Blossom server list
- Create BlossomServerListRenderer.tsx with feed and detail views
- Show user's configured Blossom servers with clickable links
- Clicking a server opens the Blossom window with server info
- Register renderers for kind 10063 (BUD-03)
- Fix lint error by renaming useFallbackServers to applyFallbackServers
* Add individual server view and NIP-05 support for blossom commands
- Add 'server' subcommand to view info about a specific Blossom server
- Update BlossomServerListRenderer to open server view on click
- Make blossom parser async to support NIP-05 resolution in 'list' command
- Add kind 10063 (Blossom Server List) to EVENT_KINDS constants with BUD-03 reference
- Update command examples with NIP-05 identifier support
* Add comprehensive tests for blossom-parser
- 34 test cases covering all subcommands (servers, server, upload, list, blob, mirror, delete)
- Tests for NIP-05 resolution, npub/nprofile decoding, $me alias
- Tests for error handling and input validation
- Tests for case insensitivity and command aliases (ls, view, rm)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Implements rich text editing with profile mentions, NIP-29 system messages,
day markers, and naddr support for a more complete chat experience.
Editor Features:
- TipTap-based rich text editor with @mention autocomplete
- FlexSearch-powered profile search (case-insensitive)
- Converts mentions to nostr:npub URIs on submission
- Keyboard navigation (Arrow keys, Enter, Escape)
- Fixed Enter key and Send button submission
NIP-29 Chat Improvements:
- System messages for join/leave events (kinds 9000, 9001, 9021, 9022)
- Styled system messages aligned left with muted text
- Shows "joined" instead of "was added" for consistency
- Accepts kind 39000 naddr (group metadata addresses)
- Day markers between messages from different days
- Day markers use locale-aware formatting (short month, no year)
Components:
- src/components/editor/MentionEditor.tsx - TipTap editor with mention support
- src/components/editor/ProfileSuggestionList.tsx - Autocomplete dropdown
- src/services/profile-search.ts - FlexSearch service for profile indexing
- src/hooks/useProfileSearch.ts - React hook for profile search
Dependencies:
- @tiptap/react, @tiptap/starter-kit, @tiptap/extension-mention
- @tiptap/extension-placeholder, @tiptap/suggestion
- flexsearch@0.7.43, tippy.js@6.3.7
Tests:
- Added 6 new tests for naddr parsing in NIP-29 adapter
- All 710 tests passing
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(CommandLauncher): redirect to dashboard when executing commands from preview routes
NIP-19 preview routes (/nevent..., /npub..., etc.) don't render the
window system, so commands executed there appeared to do nothing.
Now detects preview routes and navigates to the dashboard before
creating the window, so the command actually takes effect.
* fix(test): add WebSocket polyfill for Node.js test environment
nostr-tools relay code requires WebSocket which isn't available in
Node.js by default. Adding the ws package polyfill prevents
"ReferenceError: WebSocket is not defined" errors in tests.
* fix(test): add @types/ws and fix type cast for WebSocket polyfill
---------
Co-authored-by: Claude <noreply@anthropic.com>
Update applesauce-react from ^5.0.0 to ^5.0.1 to get latest fixes.
React 19 (^19.2.1) is already in use.
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add preview routes for Nostr identifiers (npub, nevent, note, naddr)
This commit adds dedicated preview routes for Nostr identifiers at the root level:
- /npub... - Shows a single profile view for npub identifiers
- /nevent... - Shows a single event detail view for nevent identifiers
- /note... - Shows a single event detail view for note identifiers
- /naddr... - Redirects spellbooks (kind 30777) to /:actor/:identifier route
Key changes:
- Created PreviewProfilePage component for npub identifiers
- Created PreviewEventPage component for nevent/note identifiers
- Created PreviewAddressPage component for naddr redirects
- Added hideBottomBar prop to AppShell to hide tabs in preview mode
- Added routes to root.tsx for all identifier types
Preview pages don't show bottom tabs and don't affect user's workspace layout.
* chore: update package-lock.json
* refactor: create reusable useNip19Decode hook and improve preview pages
This commit makes the preview pages production-ready by:
1. Created useNip19Decode hook (src/hooks/useNip19Decode.ts):
- Reusable hook for decoding NIP-19 identifiers (npub, note, nevent, naddr, nprofile)
- Type-safe with discriminated union for decoded entities
- Comprehensive error handling with retry functionality
- Loading states and error messages
- Well-documented with JSDoc comments and usage examples
2. Comprehensive test coverage (src/hooks/useNip19Decode.test.ts):
- 11 tests covering all entity types (npub, note, nevent, naddr)
- Tests for error handling (missing identifier, invalid format, corrupted bech32)
- Tests for retry functionality and state changes
- Uses jsdom environment for React hook testing
- All tests passing ✓
3. Refactored preview pages to use the hook:
- PreviewProfilePage: Simplified from 80 to 81 lines with cleaner logic
- PreviewEventPage: Improved type safety and error handling
- PreviewAddressPage: Better separation of concerns
- All pages now have consistent error handling and retry functionality
- Better user experience with improved error messages
4. Dependencies added:
- @testing-library/react for React hook testing
- @testing-library/dom for DOM testing utilities
- jsdom and happy-dom for browser environment simulation in tests
Benefits:
- Code deduplication: Preview pages share decoding logic
- Type safety: Discriminated union prevents type errors
- Testability: Hook can be tested independently
- Maintainability: Single source of truth for NIP-19 decoding
- User experience: Consistent error handling and retry across all preview pages
- Production-ready: Comprehensive tests and error handling
* refactor: simplify useNip19Decode to synchronous with memoization
NIP-19 decoding is synchronous - removed unnecessary async complexity:
Hook changes (src/hooks/useNip19Decode.ts):
- Removed loading states (isLoading, setIsLoading)
- Removed retry functionality (unnecessary for sync operations)
- Now uses useMemo for efficient memoization
- Returns { decoded, error } instead of { decoded, isLoading, error, retry }
- Same string always yields same result (memoized)
- Went from ~120 lines to ~115 lines, but much simpler
Preview page changes:
- Removed loading spinners and states
- Removed retry buttons
- Simplified error handling
- Cleaner, more readable code
- PreviewProfilePage: 55 lines (down from 81)
- PreviewEventPage: 83 lines (down from 105)
- PreviewAddressPage: 83 lines (down from 117)
Test changes (src/hooks/useNip19Decode.test.ts):
- Removed waitFor and async/await (not needed for sync)
- Tests run faster (39ms vs 77ms - 49% improvement)
- Added memoization tests to verify caching works
- Simplified from 11 async tests to 11 sync tests
- All 11 tests passing ✓
Benefits:
- Simpler mental model: decode happens instantly
- Better performance: no state updates, just memoization
- Easier to test: synchronous tests are simpler
- More correct: matches the actual synchronous nature of nip19.decode()
- Less code: removed ~150 lines of unnecessary complexity
* feat: show detail view for all addressable events in naddr preview
Previously, PreviewAddressPage only handled spellbooks (kind 30777) and
showed errors for other addressable events. Now:
- Spellbooks (kind 30777): Redirect to /:actor/:identifier (existing behavior)
- All other addressable events: Show in EventDetailViewer
This enables previewing any addressable event (long-form articles, live
events, community posts, etc.) via naddr links.
Changes:
- Import EventDetailViewer
- Removed error state for non-spellbook kinds
- Show EventDetailViewer with AddressPointer for all other kinds
- Simplified from 83 lines to 77 lines
* fix: correct route patterns for NIP-19 identifier previews
The previous route patterns (/npub:identifier) conflicted with the catch-all
/:actor/:identifier route and didn't properly match NIP-19 identifiers.
Fixed by:
1. Using wildcard routes with correct prefixes:
- /npub1* (not /npub:identifier)
- /nevent1* (not /nevent:identifier)
- /note1* (not /note:identifier)
- /naddr1* (not /naddr:identifier)
2. Updated preview components to use params['*'] for wildcard capture:
- Reconstruct full identifier as prefix + captured part
- e.g., npub1 + params['*'] = npub107jk7htfv...
This ensures routes properly match before the catch-all /:actor/:identifier
route and correctly capture the full bech32-encoded identifier.
Test URL: /npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg
* style: apply prettier formatting
* fix: use loader-based routing for NIP-19 identifiers in React Router v7
Previous attempts using wildcard routes didn't work properly in React Router v7.
Solution:
- Single /:identifier route with a loader that validates NIP-19 prefixes
- Loader throws 404 if identifier doesn't start with npub1/note1/nevent1/naddr1
- Created Nip19PreviewRouter component that routes to correct preview page
- Routes are properly ordered: /:identifier before /:actor/:identifier catch-all
This ensures /npub107jk... routes to profile preview, not spellbook route.
Benefits:
- Simpler routing configuration (1 route vs 4 duplicate routes)
- Proper validation via loader
- Clean separation of concerns with router component
- Works correctly in React Router v7
---------
Co-authored-by: Claude <noreply@anthropic.com>
Use fake-indexeddb to provide IndexedDB API in Node.js test environment.
This fixes 10 failing tests in spellbook-storage.test.ts that were
previously blocked by missing IndexedDB.
Changes:
- Add fake-indexeddb as dev dependency
- Create vitest setup file that imports the polyfill
- Update vitest.config.ts to use setup file
- Fix relay-selection.test.ts to clear cache between tests for isolation
Sharing Enhancements:
- Install qrcode library for QR code generation
- Create ShareSpellbookDialog with tabbed interface
- Support multiple share formats: Web URL, naddr, nevent
- QR code generation and download for each format
- Quick copy buttons with visual feedback
- Integrated into SpellbookDetailRenderer
Network Discovery:
- Add "Discover" filter to browse spellbooks from other users
- Query AGGREGATOR_RELAYS for network spellbook discovery
- Show author names using UserName component
- Conditional UI: hide owner actions for discovered spellbooks
- Support viewing and applying layouts from the community
Preview Route Polish:
- Loading states with spinner during NIP-05 resolution
- 10-second timeout for NIP-05 resolution
- Error banners for resolution failures
- Author name and creation date in preview banner
- Copy link button in preview mode
Conflict Resolution:
- compareSpellbookVersions() function in spellbook-manager
- ConflictResolutionDialog component for version conflicts
- Side-by-side comparison of local vs network versions
- Show workspace/window counts and timestamps
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update spellbook icon to BookHeart across the app
- Implement Preview mode with routing /:actor/:identifier
- Add Preview banner in Home component with Apply/Discard actions
- Add Preview and Share buttons to Spellbook renderers
- Clean up unused imports
- Display option flags on separate lines with indented descriptions to prevent overflow
- Parse and separate example commands from their descriptions
- Highlight commands in accent color with muted descriptions below
- Increase spacing between items for better readability