From 5a3e321400556ce1ab33ec3d58bd4dd81c9ecec8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 11:13:30 +0000 Subject: [PATCH] chore: remove planning docs --- .../DESIGN_editable-commands-and-history.md | 870 ---------- .../ENHANCEMENT_open-nevent-with-metadata.md | 276 ---- claudedocs/FIX_open-command-reconstruction.md | 201 --- .../IMPLEMENTATION_editable-commands-final.md | 328 ---- .../IMPLEMENTATION_editable-commands-mvp.md | 223 --- .../PLAN-repository-tree-visualization.md | 589 ------- claudedocs/PLAN-shiki-migration.md | 699 --------- claudedocs/THEME_TOKEN_SYSTEM.md | 812 ---------- claudedocs/event-rendering-system-analysis.md | 1127 ------------- claudedocs/keyboard-navigation-plan.md | 1398 ----------------- claudedocs/kind-1337-code-snippets.md | 148 -- claudedocs/outbox-future-improvements.md | 690 -------- 12 files changed, 7361 deletions(-) delete mode 100644 claudedocs/DESIGN_editable-commands-and-history.md delete mode 100644 claudedocs/ENHANCEMENT_open-nevent-with-metadata.md delete mode 100644 claudedocs/FIX_open-command-reconstruction.md delete mode 100644 claudedocs/IMPLEMENTATION_editable-commands-final.md delete mode 100644 claudedocs/IMPLEMENTATION_editable-commands-mvp.md delete mode 100644 claudedocs/PLAN-repository-tree-visualization.md delete mode 100644 claudedocs/PLAN-shiki-migration.md delete mode 100644 claudedocs/THEME_TOKEN_SYSTEM.md delete mode 100644 claudedocs/event-rendering-system-analysis.md delete mode 100644 claudedocs/keyboard-navigation-plan.md delete mode 100644 claudedocs/kind-1337-code-snippets.md delete mode 100644 claudedocs/outbox-future-improvements.md diff --git a/claudedocs/DESIGN_editable-commands-and-history.md b/claudedocs/DESIGN_editable-commands-and-history.md deleted file mode 100644 index e404b8f..0000000 --- a/claudedocs/DESIGN_editable-commands-and-history.md +++ /dev/null @@ -1,870 +0,0 @@ -# Design: Editable Window Commands & Command History - -**Created:** 2025-12-12 -**Status:** Design Complete - Ready for Implementation -**Complexity:** High (5 implementation phases) - -## Overview - -Enable users to edit the command that created a window (e.g., change `profile alice@domain.com` to `profile bob@domain.com`) and provide full undo/redo capabilities for all window operations. - ---- - -## Table of Contents - -1. [Editable Commands Architecture](#1-editable-commands-architecture) -2. [Command History Architecture](#2-command-history-architecture) -3. [User Interface Components](#3-user-interface-components) -4. [Implementation Phases](#4-implementation-phases) -5. [Key Design Decisions](#5-key-design-decisions--rationale) -6. [Edge Cases](#6-edge-cases-handled) -7. [Testing Strategy](#7-testing-strategy) -8. [Performance](#8-performance-considerations) -9. [Documentation Updates](#9-documentation-updates) - ---- - -## 1. Editable Commands Architecture - -### Schema Changes - -```typescript -// src/types/app.ts - Add commandString to WindowInstance -interface WindowInstance { - id: string; - appId: AppId; - title: string; - props: any; - commandString?: string; // NEW: Original command (e.g., "profile alice@domain.com") -} -``` - -**Backward Compatibility:** Optional field (?) means existing windows continue working without migration. - -### State Management - -```typescript -// src/core/logic.ts - New function -export const updateWindow = ( - state: GrimoireState, - windowId: string, - updates: { - props?: any; - title?: string; - commandString?: string; - appId?: AppId; - } -): GrimoireState => { - const window = state.windows[windowId]; - if (!window) return state; - - return { - ...state, - windows: { - ...state.windows, - [windowId]: { ...window, ...updates }, - }, - }; -}; -``` - -### Command Flow - -``` -User types command - ↓ -CommandLauncher parses input - ↓ -Async argParser resolves (e.g., NIP-05) - ↓ -addWindow(appId, props, title, commandString) - ↓ -Window created with commandString stored - ↓ -User clicks edit button (⌘E) - ↓ -EditCommandDialog shows commandString - ↓ -User edits and submits - ↓ -Re-parse command (async) - ↓ -updateWindow(windowId, newProps, newTitle, newCommandString) -``` - ---- - -## 2. Command History Architecture - -### Data Structures - -```typescript -// src/types/history.ts - -type HistoryAction = - | { type: 'COMMAND_EXECUTED'; commandString: string; windowId?: string } - | { type: 'WINDOW_CLOSED'; windowId: string } - | { type: 'WINDOW_EDITED'; windowId: string; oldCommand: string; newCommand: string } - | { type: 'WORKSPACE_CREATED'; workspaceId: string; label: string } - | { type: 'LAYOUT_CHANGED'; workspaceId: string }; - -interface HistoryEntry { - id: string; - timestamp: number; - action: HistoryAction; - stateBefore: GrimoireState; // For undo - stateAfter: GrimoireState; // For redo -} - -interface CommandHistory { - entries: HistoryEntry[]; // Newest first - currentIndex: number; // -1 = present, 0+ = steps back in time - maxEntries: number; // Default: 50 (circular buffer) -} -``` - -### History State Machine - -``` -Present State (currentIndex = -1) - │ - ├─ Undo → currentIndex = 0 (1 step back) - │ └─ Undo → currentIndex = 1 (2 steps back) - │ ├─ Redo → currentIndex = 0 - │ └─ New Action → Truncate future, currentIndex = -1 - │ - └─ New Action → Add entry, currentIndex = -1 -``` - -**Key Insight:** currentIndex is a time cursor. -1 means "present", positive numbers mean "N steps back in history." - -### Core History Functions - -```typescript -// src/core/history.ts - -export const recordAction = ( - history: CommandHistory, - action: HistoryAction, - stateBefore: GrimoireState, - stateAfter: GrimoireState -): CommandHistory => { - // Truncate future if in past state (Git-style) - const entries = history.currentIndex === -1 - ? history.entries - : history.entries.slice(history.currentIndex + 1); - - const newEntry: HistoryEntry = { - id: uuidv4(), - timestamp: Date.now(), - action, - stateBefore, - stateAfter, - }; - - // Circular buffer: keep only last N entries - const newEntries = [newEntry, ...entries].slice(0, history.maxEntries); - - return { - ...history, - entries: newEntries, - currentIndex: -1, // Back to present - }; -}; - -export const undo = ( - history: CommandHistory, - currentState: GrimoireState -): { history: CommandHistory; newState: GrimoireState } => { - if (history.currentIndex >= history.entries.length - 1) { - return { history, newState: currentState }; // Can't undo further - } - - const targetIndex = history.currentIndex + 1; - const targetEntry = history.entries[targetIndex]; - - return { - history: { ...history, currentIndex: targetIndex }, - newState: targetEntry.stateBefore, - }; -}; - -export const redo = ( - history: CommandHistory, - currentState: GrimoireState -): { history: CommandHistory; newState: GrimoireState } => { - if (history.currentIndex <= -1) { - return { history, newState: currentState }; // Already at present - } - - const currentEntry = history.entries[history.currentIndex]; - - return { - history: { ...history, currentIndex: history.currentIndex - 1 }, - newState: currentEntry.stateAfter, - }; -}; -``` - -### Integration with State Updates - -```typescript -// src/core/state.ts - -export const useGrimoire = () => { - const [state, setState] = useAtom(grimoireStateAtom); - const [history, setHistory] = useAtom(historyAtom); - - const addWindow = useCallback((appId: AppId, props: any, title?: string, commandString?: string) => { - setState((prevState) => { - const nextState = Logic.addWindow(prevState, { - appId, - props, - title: title || appId.toUpperCase(), - commandString - }); - - // Record history atomically - setHistory(prev => History.recordAction(prev, { - type: 'COMMAND_EXECUTED', - commandString: commandString || generateRawCommand(appId, props), - }, prevState, nextState)); - - return nextState; - }); - }, [setState, setHistory]); - - const performUndo = useCallback(() => { - const { history: newHistory, newState } = History.undo(history, state); - setState(newState); - setHistory(newHistory); - }, [history, state, setState, setHistory]); - - const performRedo = useCallback(() => { - const { history: newHistory, newState } = History.redo(history, state); - setState(newState); - setHistory(newHistory); - }, [history, state, setState, setHistory]); - - return { - state, - addWindow, - updateWindow, - performUndo, - performRedo, - // ... other methods - }; -}; -``` - ---- - -## 3. User Interface Components - -### Edit Command Dialog - -```typescript -// src/components/EditCommandDialog.tsx - -interface EditCommandDialogProps { - window: WindowInstance; - open: boolean; - onClose: () => void; -} - -export function EditCommandDialog({ window, open, onClose }: EditCommandDialogProps) { - const [input, setInput] = useState(window.commandString || ''); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const { updateWindow } = useGrimoire(); - - const handleSubmit = async () => { - setIsLoading(true); - setError(null); - - try { - const parts = input.trim().split(/\s+/); - const commandName = parts[0]; - const args = parts.slice(1); - - const command = manPages[commandName]; - if (!command) { - throw new Error(`Unknown command: ${commandName}`); - } - - // Async parsing support (e.g., NIP-05 resolution) - const props = command.argParser - ? await Promise.resolve(command.argParser(args)) - : command.defaultProps || {}; - - const title = args.length > 0 - ? `${commandName.toUpperCase()} ${args.join(' ')}` - : commandName.toUpperCase(); - - updateWindow(window.id, { - props, - title, - commandString: input, - appId: command.appId, - }); - - onClose(); - } catch (err) { - setError(err.message); - } finally { - setIsLoading(false); - } - }; - - return ( - - - Edit Command - setInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} - placeholder="Enter command..." - disabled={isLoading} - autoFocus - /> - {error &&

{error}

} - {isLoading &&

Parsing command...

} - - - - -
-
- ); -} -``` - -### Window Toolbar Edit Button - -```typescript -// src/components/WindowToolbar.tsx - Add edit button - -export function WindowToolbar({ windowId, onClose }: WindowToolbarProps) { - const [showEditDialog, setShowEditDialog] = useState(false); - const window = useWindowInstance(windowId); - - return ( - <> -
- - -
- - setShowEditDialog(false)} - /> - - ); -} -``` - -### Command Palette History Navigation - -```typescript -// src/components/CommandLauncher.tsx - Add arrow key navigation - -export function CommandLauncher({ open, onOpenChange }: CommandLauncherProps) { - const [input, setInput] = useState(''); - const [historyIndex, setHistoryIndex] = useState(-1); - const history = useCommandHistory(); - - // Get command history (only COMMAND_EXECUTED actions) - const commandHistory = useMemo(() => - history.entries - .filter(e => e.action.type === 'COMMAND_EXECUTED') - .map(e => e.action.commandString) - .reverse(), // Most recent first - [history]); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - executeCommand(); - setHistoryIndex(-1); - return; - } - - if (e.key === 'ArrowUp' && historyIndex < commandHistory.length - 1) { - e.preventDefault(); - const newIndex = historyIndex + 1; - setHistoryIndex(newIndex); - setInput(commandHistory[newIndex]); - } - - if (e.key === 'ArrowDown') { - e.preventDefault(); - if (historyIndex > 0) { - const newIndex = historyIndex - 1; - setHistoryIndex(newIndex); - setInput(commandHistory[newIndex]); - } else if (historyIndex === 0) { - setHistoryIndex(-1); - setInput(''); // Clear when going past most recent - } - } - }; - - // ... rest of component -} -``` - -### Keyboard Shortcuts - -```typescript -// src/hooks/useGlobalKeyboardShortcuts.ts - -export function useGlobalKeyboardShortcuts() { - const { performUndo, performRedo } = useGrimoire(); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Undo: Cmd+Z (Mac) or Ctrl+Z (Windows/Linux) - if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) { - e.preventDefault(); - performUndo(); - } - - // Redo: Cmd+Shift+Z (Mac) or Ctrl+Shift+Z (Windows/Linux) - if ((e.metaKey || e.ctrlKey) && e.key === 'z' && e.shiftKey) { - e.preventDefault(); - performRedo(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [performUndo, performRedo]); -} -``` - ---- - -## 4. Implementation Phases - -### Phase 1: Editable Commands Foundation ✅ -**Goal:** Store command strings and enable programmatic updates - -**Tasks:** -- [ ] Add `commandString?: string` to `WindowInstance` type -- [ ] Implement `updateWindow()` in `src/core/logic.ts` -- [ ] Add `updateWindow` to `useGrimoire` hook -- [ ] Modify `CommandLauncher` to pass `commandString` to `addWindow` -- [ ] Write unit tests for `updateWindow` - -**Files to modify:** -- `src/types/app.ts` -- `src/core/logic.ts` -- `src/core/state.ts` -- `src/components/CommandLauncher.tsx` -- `src/core/logic.test.ts` (new) - -**Estimated effort:** 2-3 hours - ---- - -### Phase 2: Edit Command UI 🎯 -**Goal:** User-facing command editing feature - -**Tasks:** -- [ ] Create `EditCommandDialog` component -- [ ] Add edit button to `WindowToolbar` -- [ ] Add ⌘E keyboard shortcut for focused window -- [ ] Handle async command parsing (loading states) -- [ ] Add error handling and validation - -**Files to create:** -- `src/components/EditCommandDialog.tsx` - -**Files to modify:** -- `src/components/WindowToolbar.tsx` -- `src/hooks/useWindowKeyboardShortcuts.ts` (new) - -**Estimated effort:** 3-4 hours - -**Deliverable:** Working command editing feature! Users can edit window commands. - ---- - -### Phase 3: History Infrastructure 📊 -**Goal:** Track all state changes for undo/redo - -**Tasks:** -- [ ] Create history types in `src/types/history.ts` -- [ ] Implement history atom with `atomWithStorage` -- [ ] Implement `recordAction`, `undo`, `redo` functions -- [ ] Integrate history recording into state mutations -- [ ] Write comprehensive unit tests - -**Files to create:** -- `src/types/history.ts` -- `src/core/history.ts` -- `src/hooks/useHistory.ts` -- `src/core/history.test.ts` - -**Files to modify:** -- `src/core/state.ts` (record history on all mutations) - -**Estimated effort:** 4-5 hours - ---- - -### Phase 4: Undo/Redo UI ⚡ -**Goal:** User-facing undo/redo functionality - -**Tasks:** -- [ ] Add global keyboard shortcuts (⌘Z, ⌘⇧Z) -- [ ] Add toast notifications for undo/redo feedback -- [ ] Add visual history indicator (optional) -- [ ] Test with various window operations - -**Files to create:** -- `src/hooks/useGlobalKeyboardShortcuts.ts` - -**Files to modify:** -- `src/App.tsx` - -**Estimated effort:** 2-3 hours - -**Deliverable:** Full undo/redo system with keyboard shortcuts! - ---- - -### Phase 5: History Viewer & Polish 🎨 -**Goal:** Complete history experience - -**Tasks:** -- [ ] Create `HistoryViewer` component (timeline UI) -- [ ] Add `history` command to `manPages` -- [ ] Add command palette history navigation (↑/↓) -- [ ] Add context menu for windows (right-click) -- [ ] Performance optimization if needed - -**Files to create:** -- `src/components/viewers/HistoryViewer.tsx` - -**Files to modify:** -- `src/types/man.ts` -- `src/components/CommandLauncher.tsx` (enhance) - -**Estimated effort:** 3-4 hours - -**Deliverable:** Full history viewer and enhanced command palette! - ---- - -**Total estimated effort:** 14-19 hours across 5 phases - ---- - -## 5. Key Design Decisions & Rationale - -| Decision | Rationale | -|----------|-----------| -| **Store commandString in WindowInstance** | Preserves exact user input, enables lossless editing. Optional field ensures backward compatibility. | -| **Separate history atom** | Clean separation of concerns, independent persistence, easier to add limits without affecting main state. | -| **Full state snapshots (not deltas)** | Simpler implementation, reliable undo/redo. Structural sharing keeps memory reasonable. 50-entry circular buffer prevents unbounded growth. | -| **Git-style history (truncate future)** | Simple mental model matching text editors. Users understand "undo to desired state, continue from there." | -| **Explicit history recording** | Reliable and predictable. Action metadata (command string, window ID) readily available. | -| **Async command parsing support** | Commands like `profile alice@domain.com` require NIP-05 resolution. EditDialog shows loading state during async operations. | -| **localStorage for history** | Simple, persistent, works offline. Can migrate to IndexedDB if needed for unlimited history. | -| **currentIndex = -1 for present** | Clear distinction between "at present" vs "in past". Makes redo logic simpler. | - ---- - -## 6. Edge Cases Handled - -| Edge Case | Solution | -|-----------|----------| -| **Old windows without commandString** | Use `generateRawCommand()` as fallback. Optionally mark as "[reconstructed]" in UI. | -| **Async command parsing** | Show loading state in EditCommandDialog. Handle errors gracefully with error message display. | -| **Invalid command after edit** | Parse validation before updating. Display error message, don't update window. | -| **New action after undo** | Truncate future entries (Git-style). User proceeds from undone state. | -| **History memory limits** | Circular buffer with maxEntries (50). Oldest entries automatically dropped. | -| **Layout changes without commands** | Debounce layout changes, record LAYOUT_CHANGED action after 1s of inactivity. | -| **localStorage quota exceeded** | Jotai's storage handles gracefully. Can implement compression if needed (LZ-string). | -| **Command that opens multiple windows** | HistoryAction supports `windowIds: string[]` for future extension. | -| **Undo/redo keyboard shortcuts conflict** | Use standard shortcuts (⌘Z, ⌘⇧Z) that don't conflict with browser or other app shortcuts. | - ---- - -## 7. Testing Strategy - -### Unit Tests - -```typescript -// src/core/history.test.ts -describe('History System', () => { - describe('recordAction', () => { - it('adds entry to history', () => { ... }); - it('maintains maxEntries limit', () => { ... }); - it('truncates future when in past state', () => { ... }); - it('stores both stateBefore and stateAfter', () => { ... }); - }); - - describe('undo', () => { - it('restores stateBefore', () => { ... }); - it('updates currentIndex correctly', () => { ... }); - it('returns unchanged when at limit', () => { ... }); - it('works with multiple undo operations', () => { ... }); - }); - - describe('redo', () => { - it('restores stateAfter', () => { ... }); - it('returns unchanged when at present', () => { ... }); - it('works after multiple undos', () => { ... }); - }); - - describe('circular buffer', () => { - it('drops oldest entries when maxEntries exceeded', () => { ... }); - it('maintains correct currentIndex after truncation', () => { ... }); - }); -}); - -// src/core/logic.test.ts -describe('updateWindow', () => { - it('updates window props', () => { ... }); - it('updates title', () => { ... }); - it('updates commandString', () => { ... }); - it('updates appId', () => { ... }); - it('updates multiple fields at once', () => { ... }); - it('returns unchanged for nonexistent window', () => { ... }); - it('preserves other window fields', () => { ... }); -}); -``` - -### Integration Tests (Manual) - -1. **Edit Command Flow:** - - Execute `profile alice@domain.com` - - Click edit button - - Change to `profile bob@domain.com` - - Verify window updates with new profile data - -2. **Undo/Redo Flow:** - - Execute multiple commands (profile, req, open) - - Undo each command (⌘Z) - - Verify state restored correctly - - Redo each command (⌘⇧Z) - - Verify state matches original - -3. **History Navigation:** - - Open command palette - - Press ↑ multiple times - - Verify recent commands appear - - Press ↓ to go back - - Verify command changes - -4. **Async Command Editing:** - - Edit profile window with NIP-05 - - Verify loading state shown - - Verify update after resolution completes - -5. **Error Handling:** - - Edit command to invalid syntax - - Verify error message shown - - Verify window not updated - -6. **History Limits:** - - Execute >50 commands - - Verify oldest commands dropped - - Verify undo still works correctly - -### Performance Tests - -1. **History Storage Size:** - - Execute 50 commands - - Check localStorage size - - Verify <1MB (reasonable limit) - -2. **Undo/Redo Latency:** - - Measure time to undo with complex state - - Should be <50ms (imperceptible) - -3. **Command Parsing:** - - Measure async NIP-05 resolution time - - Should complete within 2-3 seconds - ---- - -## 8. Performance Considerations - -| Aspect | Performance | Notes | -|--------|-------------|-------| -| **Memory** | ~10MB for 50 entries | Within localStorage limits. Each state ~100KB. Structural sharing reduces actual memory usage. | -| **Undo/Redo latency** | <10ms | O(1) state swap, imperceptible to user. | -| **History recording** | <5ms | `structuredClone()` is native and fast. | -| **localStorage writes** | Debounced by Jotai | No performance impact on normal operations. Writes happen in background. | -| **Command parsing** | Varies (50ms - 3s) | Depends on async operations (NIP-05). Loading state shown during parse. | - -### Future Optimizations (if needed) - -1. **Delta encoding** - Store only changed fields instead of full state snapshots - - Pro: ~90% memory reduction - - Con: Complex implementation, harder debugging - -2. **LZ-string compression** - Compress state snapshots before localStorage - - Pro: 50-70% size reduction - - Con: CPU overhead for compress/decompress - -3. **IndexedDB migration** - Move from localStorage to IndexedDB - - Pro: No size limits, better performance for large datasets - - Con: More complex API, async operations - -4. **Lazy loading** - Only keep recent N entries in memory - - Pro: Reduced memory footprint - - Con: Async loading on undo, more complex code - -**Recommendation:** Start with simple approach. Optimize only if profiling shows real issues. - ---- - -## 9. Documentation Updates - -### CLAUDE.md Addition - -Add new section after "## Core Architecture": - -```markdown -### Command History & Editing - -Windows store their originating command string, enabling editing and comprehensive undo/redo. - -**Edit Commands:** -- Click pencil icon in window toolbar or press ⌘E -- Edit the command string in the dialog -- Command is re-parsed (with async support for NIP-05, etc.) -- Window updates with new data - -**Undo/Redo:** -- **⌘Z**: Undo last action (window create, close, edit, layout change) -- **⌘⇧Z**: Redo previously undone action -- History limited to last 50 actions (circular buffer) -- Git-style: new actions after undo truncate future - -**Command History:** -- **↑/↓** in command palette: Navigate through recent commands -- History stored in separate Jotai atom (`historyAtom`) -- Each entry captures state snapshots before/after for perfect undo - -**Implementation:** -- `src/core/history.ts` - History logic and storage -- `src/types/history.ts` - Type definitions -- `src/hooks/useHistory.ts` - React integration -- `src/components/EditCommandDialog.tsx` - Edit UI -``` - -### Type Documentation - -```typescript -/** - * WindowInstance represents a single window in the Grimoire application. - * Each window displays content from a specific app (profile, event, NIP viewer, etc.) - * and is created from a user command via the command palette. - * - * @property id - Unique window identifier (UUID) - * @property appId - Type of app/viewer this window displays - * @property title - Window title shown in toolbar - * @property props - App-specific properties (pubkey, filter, etc.) - * @property commandString - The original command that created this window (e.g., "profile alice@domain.com"). - * Enables command editing and history features. May be undefined for windows created - * before this feature was added. Use generateRawCommand() as a fallback. - */ -interface WindowInstance { - id: string; - appId: AppId; - title: string; - props: any; - commandString?: string; -} -``` - -### Usage Examples - -```typescript -// Example: Creating a window with command string -addWindow('profile', { pubkey: '...' }, 'PROFILE alice', 'profile alice@domain.com'); - -// Example: Updating a window -updateWindow(windowId, { - props: { pubkey: 'new-pubkey' }, - title: 'PROFILE bob', - commandString: 'profile bob@domain.com', -}); - -// Example: Undo/redo -performUndo(); // Undo last action -performRedo(); // Redo undone action - -// Example: Recording custom history action -recordHistory({ - type: 'LAYOUT_CHANGED', - workspaceId: 'workspace-id', -}, prevState, nextState); -``` - ---- - -## 10. Security & Privacy Considerations - -| Concern | Mitigation | -|---------|-----------| -| **Sensitive data in history** | History stored in localStorage (same origin only). Consider adding opt-out for privacy-conscious users. | -| **Command injection** | All commands parsed through `manPages` argParser. No arbitrary code execution. | -| **localStorage quota attacks** | Circular buffer limits history size. Jotai handles quota exceeded gracefully. | -| **XSS via command strings** | Command strings displayed in UI are properly escaped by React. | - ---- - -## 11. Future Enhancements - -### Potential Phase 6+ Features - -1. **Workspace-specific undo** - Undo operations scoped to current workspace -2. **History search** - Search through command history by text -3. **History export** - Export history as JSON for debugging -4. **Command templates** - Save frequently used commands as templates -5. **Bulk operations** - Edit multiple windows at once -6. **History persistence options** - Choose localStorage vs IndexedDB vs in-memory -7. **Visual timeline** - Graphical timeline of actions with branching -8. **Command aliases** - Create shortcuts for long commands (e.g., `alice` → `profile alice@domain.com`) - ---- - -## Summary - -This design provides a complete, production-ready system for editable window commands and comprehensive command history: - -✅ **Backward Compatible** - Optional fields, no migrations needed -✅ **Simple Mental Model** - Git-style history familiar to developers -✅ **Memory Efficient** - Circular buffer with structural sharing -✅ **Type Safe** - Full TypeScript coverage -✅ **Testable** - Pure functions, comprehensive test suite -✅ **Phased Implementation** - Each phase independently deliverable -✅ **Well Documented** - Clear API contracts and usage examples -✅ **User-Friendly** - Familiar keyboard shortcuts, visual feedback, error handling - -The architecture cleanly separates concerns (state vs history), handles edge cases gracefully, and provides excellent UX with keyboard shortcuts, visual feedback, and error handling. - -**Status:** Design complete and validated. Ready for Phase 1 implementation. - -**Next Steps:** -1. Review design with team/stakeholders -2. Create implementation branch -3. Start with Phase 1 (foundation) -4. Iterate through phases 2-5 -5. Deploy and gather user feedback diff --git a/claudedocs/ENHANCEMENT_open-nevent-with-metadata.md b/claudedocs/ENHANCEMENT_open-nevent-with-metadata.md deleted file mode 100644 index 7406851..0000000 --- a/claudedocs/ENHANCEMENT_open-nevent-with-metadata.md +++ /dev/null @@ -1,276 +0,0 @@ -# Enhancement: OPEN Command Always Uses nevent with Full Metadata - -**Date:** 2025-12-13 -**Type:** Enhancement -**Status:** ✅ Implemented - -## Overview - -Updated the OPEN command reconstruction to always generate `nevent` identifiers (never `note`) with full metadata including kind information and relay hints from seen relays. - -## Changes - -### Previous Behavior - -```typescript -// Simple events → note -open note1... - -// Events with metadata → nevent -open nevent1... -``` - -### New Behavior - -```typescript -// Always nevent with full metadata -open nevent1... // Includes: id, kind, author, seen relays -``` - -## Implementation - -### Key Updates - -1. **Import Event Store & Relay Helpers** - ```typescript - import eventStore from "@/services/event-store"; - import { getSeenRelays } from "applesauce-core/helpers/relays"; - ``` - -2. **Lookup Event in Store** - ```typescript - const event = eventStore.event(pointer.id); - ``` - -3. **Extract Seen Relays** - ```typescript - const seenRelaysSet = getSeenRelays(event); - const seenRelays = seenRelaysSet ? Array.from(seenRelaysSet) : undefined; - ``` - -4. **Always Encode as nevent with Full Metadata** - ```typescript - const nevent = nip19.neventEncode({ - id: event.id, - kind: event.kind, // ✅ Kind information - author: event.pubkey, - relays: seenRelays, // ✅ Seen relays - }); - ``` - -## Benefits - -### 1. Complete Context -nevent identifiers include all context needed to fetch the event: -- **Event ID**: Unique identifier -- **Kind**: Event type (helps with rendering) -- **Author**: Pubkey (useful for context) -- **Relay Hints**: Where the event was seen (improves fetch success rate) - -### 2. Better Relay Discovery -Using seen relays ensures the reconstructed command points to relays that actually have the event, improving fetch reliability. - -### 3. Consistency -All event references use the same format (nevent), making the system more predictable. - -### 4. Future-Proof -nevent is the recommended format for event references with context, ensuring compatibility with other Nostr tools. - -## Example Scenarios - -### Scenario 1: Event in Store with Seen Relays - -**Window State:** -```typescript -{ - pointer: { id: "abc123..." } -} -``` - -**Lookup Result:** -```typescript -event = { - id: "abc123...", - kind: 1, - pubkey: "def456...", - // ... other fields -} -seenRelays = ["wss://relay.damus.io", "wss://nos.lol"] -``` - -**Reconstructed Command:** -``` -open nevent1qqs... // Contains: id, kind:1, author, 2 relay hints -``` - -### Scenario 2: Event Not in Store (Fallback) - -**Window State:** -```typescript -{ - pointer: { - id: "abc123...", - relays: ["wss://relay.primal.net"], - author: "def456..." - } -} -``` - -**Reconstructed Command:** -``` -open nevent1qqs... // Uses stored pointer data -``` - -### Scenario 3: Addressable Events (naddr) - -**Window State:** -```typescript -{ - pointer: { - kind: 30023, - pubkey: "abc123...", - identifier: "my-article" - } -} -``` - -**Lookup Result:** -```typescript -seenRelays = ["wss://relay.nostr.band"] -``` - -**Reconstructed Command:** -``` -open naddr1... // With updated relay hints from seen relays -``` - -## Technical Details - -### Event Store Lookups - -**Regular Events:** -```typescript -const event = eventStore.event(pointer.id); -// Synchronous lookup in local cache -``` - -**Addressable/Replaceable Events:** -```typescript -const event = eventStore.replaceable( - pointer.kind, - pointer.pubkey, - pointer.identifier || "" -); -// Synchronous lookup for latest replaceable event -``` - -### Seen Relays Extraction - -```typescript -const seenRelaysSet = getSeenRelays(event); -// Returns Set of relay URLs where event was seen -// Managed by applesauce-core -``` - -### Encoding - -```typescript -// nevent encoding (EventPointer) -nip19.neventEncode({ - id: string, - kind?: number, // Optional but recommended - author?: string, // Optional but recommended - relays?: string[], // Optional but improves fetch -}); - -// naddr encoding (AddressPointer) -nip19.naddrEncode({ - kind: number, // Required - pubkey: string, // Required - identifier: string, // Required - relays?: string[], // Optional but improves fetch -}); -``` - -## Edge Cases Handled - -| Case | Behavior | -|------|----------| -| **Event in store** | Use kind & seen relays from event | -| **Event not in store** | Fallback to stored pointer data | -| **No seen relays** | Omit relays (still valid nevent) | -| **Encoding error** | Fallback to raw ID display | -| **Addressable events** | Use naddr with seen relays | - -## Performance - -- **Event lookup**: O(1) - EventStore uses Map internally -- **Seen relays**: O(1) - Cached by applesauce -- **Encoding**: <1ms - Native nip19 encoding -- **Total overhead**: <5ms per reconstruction - -## Testing - -### Manual Test Cases - -1. **Open any event**: `open note1...` or `nevent1...` -2. **Click edit button** -3. **Verify**: CommandLauncher shows `open nevent1...` with full metadata - -**Expected nevent structure:** -- Has more characters than note (includes metadata) -- When decoded, shows kind, author, and relay hints -- Relays match where event was seen - -### Verification Commands - -```typescript -// Decode the nevent to verify contents -import { nip19 } from "nostr-tools"; -const decoded = nip19.decode("nevent1..."); -console.log(decoded); -// Output: -// { -// type: "nevent", -// data: { -// id: "abc123...", -// kind: 1, -// author: "def456...", -// relays: ["wss://relay.damus.io", "wss://nos.lol"] -// } -// } -``` - -## Files Modified - -- `src/lib/command-reconstructor.ts` - - Added imports: eventStore, getSeenRelays - - Updated `open` case: Always nevent with metadata - - Enhanced `naddr` case: Include seen relays - -## Benefits Over Previous Approach - -| Aspect | Before | After | -|--------|--------|-------| -| **Format** | note or nevent | Always nevent | -| **Kind info** | ❌ Not in note | ✅ Always included | -| **Relay hints** | ⚠️ Only if stored | ✅ From seen relays | -| **Context** | Minimal | Complete | -| **Reliability** | Partial | High | - -## Future Enhancements - -- [ ] Cache reconstructed commands to avoid repeated lookups -- [ ] Prune relay list to top N most reliable relays -- [ ] Add event fetch timeout for missing events - -## Conclusion - -The OPEN command now provides complete context through nevent identifiers with: -- ✅ Event kind information -- ✅ Author pubkey -- ✅ Relay hints from actual seen relays -- ✅ Better fetch reliability -- ✅ Consistent format across all events - -This enhancement ensures users get rich, actionable command strings when editing OPEN windows. diff --git a/claudedocs/FIX_open-command-reconstruction.md b/claudedocs/FIX_open-command-reconstruction.md deleted file mode 100644 index 68ad979..0000000 --- a/claudedocs/FIX_open-command-reconstruction.md +++ /dev/null @@ -1,201 +0,0 @@ -# Fix: OPEN Command Reconstruction - -**Date:** 2025-12-13 -**Issue:** OPEN command sometimes didn't include event bech32 when clicking edit -**Status:** ✅ Fixed - -## Problem Analysis - -### Root Cause - -The command reconstructor was checking for the wrong props structure. - -**Incorrect code:** -```typescript -case "open": { - if (props.id) { ... } // ❌ Wrong! - if (props.address) { ... } // ❌ Wrong! - return "open"; -} -``` - -**Actual props structure:** -```typescript -{ - pointer: EventPointer | AddressPointer -} -``` - -Where: -- `EventPointer`: `{ id: string, relays?: string[], author?: string }` -- `AddressPointer`: `{ kind: number, pubkey: string, identifier: string, relays?: string[] }` - -### Why This Happened - -The `parseOpenCommand` function returns `{ pointer: EventPointer | AddressPointer }`, but the reconstructor was looking for `props.id` and `props.address` directly. This mismatch caused the reconstruction to fail and return just `"open"` without the event identifier. - -## Solution - -Updated the `open` case in `command-reconstructor.ts` to properly handle the pointer structure: - -### Implementation - -```typescript -case "open": { - // Handle pointer structure from parseOpenCommand - if (!props.pointer) return "open"; - - const pointer = props.pointer; - - try { - // EventPointer (has id field) - if ("id" in pointer) { - // If has relays or author metadata, use nevent - if (pointer.relays?.length || pointer.author) { - const nevent = nip19.neventEncode({ - id: pointer.id, - relays: pointer.relays, - author: pointer.author, - }); - return `open ${nevent}`; - } - // Otherwise use simple note - const note = nip19.noteEncode(pointer.id); - return `open ${note}`; - } - - // AddressPointer (has kind, pubkey, identifier) - if ("kind" in pointer) { - const naddr = nip19.naddrEncode({ - kind: pointer.kind, - pubkey: pointer.pubkey, - identifier: pointer.identifier, - relays: pointer.relays, - }); - return `open ${naddr}`; - } - } catch (error) { - console.error("Failed to encode open command:", error); - // Fallback to raw pointer display - if ("id" in pointer) { - return `open ${pointer.id}`; - } - } - - return "open"; -} -``` - -## Encoding Strategy - -### EventPointer (has `id`) - -1. **With metadata** (relays or author) → Encode as `nevent` - - Input: `{ id: "abc...", relays: ["wss://relay.com"], author: "def..." }` - - Output: `open nevent1...` - - Preserves relay hints and author information - -2. **Without metadata** → Encode as `note` - - Input: `{ id: "abc..." }` - - Output: `open note1...` - - Simpler, more common format - -### AddressPointer (has `kind`, `pubkey`, `identifier`) - -- Always encode as `naddr` -- Input: `{ kind: 30023, pubkey: "abc...", identifier: "my-article" }` -- Output: `open naddr1...` -- Used for replaceable/parameterized replaceable events - -## Test Cases - -### Test 1: Simple Event (note) -```typescript -// Window with EventPointer (just ID) -{ - pointer: { id: "abc123..." } -} -// Reconstructs to: -"open note1..." -``` - -### Test 2: Event with Metadata (nevent) -```typescript -// Window with EventPointer (with relays/author) -{ - pointer: { - id: "abc123...", - relays: ["wss://relay.damus.io"], - author: "def456..." - } -} -// Reconstructs to: -"open nevent1..." -``` - -### Test 3: Addressable Event (naddr) -```typescript -// Window with AddressPointer -{ - pointer: { - kind: 30023, - pubkey: "abc123...", - identifier: "my-article", - relays: ["wss://relay.nostr.band"] - } -} -// Reconstructs to: -"open naddr1..." -``` - -### Test 4: Original Hex Input -```typescript -// User typed: open abc123... (hex) -// Parser creates: { pointer: { id: "abc123..." } } -// Reconstructs to: "open note1..." (encoded as bech32) -// ✅ Better UX - consistent bech32 format -``` - -## Why This Fix Works - -1. **Correct Props Structure**: Now checks `props.pointer` instead of non-existent `props.id` -2. **Type Detection**: Uses `"id" in pointer` vs `"kind" in pointer` to distinguish types -3. **Smart Encoding**: - - Uses `nevent` when metadata exists (preserves relay hints) - - Uses `note` for simple cases (cleaner) - - Uses `naddr` for addressable events (required format) -4. **Error Handling**: Fallback to raw ID if encoding fails -5. **Consistent Output**: All reconstructed commands use bech32 format - -## Impact - -### Before Fix -- Clicking edit on open windows showed just `"open"` with no event ID -- Users couldn't edit open commands -- Command reconstruction was broken for all open windows - -### After Fix -- ✅ Clicking edit shows full command: `open note1...` / `nevent1...` / `naddr1...` -- ✅ Users can edit and resubmit open commands -- ✅ Preserves relay hints and metadata when present -- ✅ Consistent with how users type commands - -## Files Changed - -- `src/lib/command-reconstructor.ts` - Fixed `open` case (lines 39-81) - -## Verification - -✅ TypeScript compilation successful -✅ No breaking changes -✅ Backward compatible (handles both old and new windows) - -## Related Components - -- `src/lib/open-parser.ts` - Defines pointer structures and parsing logic -- `src/components/EventDetailViewer.tsx` - Consumes pointer prop -- `src/types/man.ts` - Defines open command entry - -## Future Improvements - -None needed - the fix is complete and handles all cases properly. diff --git a/claudedocs/IMPLEMENTATION_editable-commands-final.md b/claudedocs/IMPLEMENTATION_editable-commands-final.md deleted file mode 100644 index c719b83..0000000 --- a/claudedocs/IMPLEMENTATION_editable-commands-final.md +++ /dev/null @@ -1,328 +0,0 @@ -# Editable Commands - Final Implementation - -**Date:** 2025-12-13 -**Status:** ✅ Complete -**Approach:** Reuse CommandLauncher with prefilled commands + command reconstruction - -## Overview - -Implemented editable window commands by reusing the familiar CommandLauncher interface. Users click the edit button and the CommandLauncher opens prefilled with the window's command, providing a seamless editing experience. Includes intelligent command reconstruction for windows created before command tracking was added. - -## Key Design Decisions - -### 1. Reuse CommandLauncher Instead of Custom Dialog - -**Rationale:** -- ✅ Users already know how to use CommandLauncher -- ✅ Consistent UX across command creation and editing -- ✅ No duplicate UI code -- ✅ All CommandLauncher features available (suggestions, parsing hints, etc.) - -**Implementation:** -- Edit mode state managed via Jotai atoms -- CommandLauncher detects edit mode and prefills input -- Updates existing window instead of creating new one - -### 2. Command Reconstruction for Old Windows - -**Problem:** Windows created before commandString tracking have no stored command. - -**Solution:** Intelligent reconstruction based on appId and props: - -```typescript -// Examples of reconstruction: -profile window → "profile npub1..." (encodes hex to npub) -req window → "req -k 1,3 -l 50 -a npub1..." (reconstructs all flags) -nip window → "nip 19" -open window → "open note1..." (encodes hex to note) -``` - -**Coverage:** -- ✅ Simple commands: nip, kind, man, kinds, conn, help -- ✅ Profile commands: with npub encoding -- ✅ Open commands: with note/naddr encoding -- ✅ Complex req commands: all flags reconstructed -- ✅ Relay, encode, decode commands - -## Architecture - -### State Management via Jotai Atoms - -**New atom:** -```typescript -// src/core/command-launcher-state.ts -export interface EditModeState { - windowId: string; - initialCommand: string; -} - -export const commandLauncherEditModeAtom = atom(null); -``` - -**Flow:** -1. User clicks edit button → WindowToolbar sets edit mode atom -2. WindowToolbar opens CommandLauncher -3. CommandLauncher detects edit mode, prefills command -4. User edits and submits -5. CommandLauncher calls updateWindow instead of addWindow -6. Edit mode atom cleared - -### Component Integration - -``` -WindowToolbar (edit button) - ↓ (sets editMode atom) - ↓ (calls onEditCommand) -Home.tsx (opens CommandLauncher) - ↓ -CommandLauncher (reads editMode atom) - ↓ (prefills input) - ↓ (user edits) - ↓ (calls updateWindow) -Window updated! -``` - -## Implementation Details - -### Files Created (2) - -1. **`src/lib/command-reconstructor.ts`** (245 lines) - - `reconstructCommand(window)` - Main reconstruction function - - `reconstructReqCommand(props)` - Complex req command reconstruction - - Handles all command types with intelligent encoding (npub, note, naddr) - -2. **`src/core/command-launcher-state.ts`** (13 lines) - - Edit mode state atom - - Clean separation of concerns - -### Files Modified (4) - -1. **`src/components/CommandLauncher.tsx`** - - Added edit mode detection via atom - - Prefills input when in edit mode - - Calls updateWindow vs addWindow based on mode - - Clears edit mode after execution - -2. **`src/components/WindowToolbar.tsx`** - - Edit button triggers edit mode - - Uses reconstructCommand for old windows - - Sets edit mode atom and opens launcher - -3. **`src/components/WindowTitle.tsx`** - - Passes onEditCommand callback to toolbar - -4. **`src/components/Home.tsx`** - - Passes CommandLauncher opener to WindowTile - -### Files Removed (1) - -- **`src/components/EditCommandDialog.tsx`** - No longer needed! - -## Command Reconstruction Examples - -### Simple Commands -```typescript -// NIP window -{ appId: "nip", props: { number: "19" } } -→ "nip 19" - -// Kind window -{ appId: "kind", props: { number: "1" } } -→ "kind 1" - -// Man window -{ appId: "man", props: { cmd: "profile" } } -→ "man profile" -``` - -### Profile Command with Encoding -```typescript -// Profile window with hex pubkey -{ - appId: "profile", - props: { pubkey: "abc123..." } -} -→ "profile npub1..." // Encoded as npub for readability -``` - -### Complex Req Command -```typescript -// Req window with multiple filters -{ - appId: "req", - props: { - filter: { - kinds: [1, 3], - authors: ["abc..."], - limit: 50, - "#t": ["nostr", "bitcoin"] - }, - relays: ["relay.damus.io"] - } -} -→ "req -k 1,3 -a npub1... -l 50 -t nostr,bitcoin relay.damus.io" -``` - -### Open Command with Encoding -```typescript -// Open window with event ID -{ - appId: "open", - props: { id: "def456..." } -} -→ "open note1..." // Encoded as note for consistency -``` - -## User Experience - -### Editing a Window - -1. **Click edit button** (Pencil icon) in any window toolbar -2. **CommandLauncher opens** with command prefilled -3. **Edit command** using familiar interface with: - - Command suggestions - - Syntax hints - - Real-time parsing feedback -4. **Press Enter** or click away -5. **Window updates instantly** with new data - -### Old Windows (No commandString) - -For windows created before command tracking: -1. Click edit button -2. Command is **automatically reconstructed** from window data -3. Edit reconstructed command normally -4. Window updates and now has commandString saved - -## Edge Cases Handled - -| Edge Case | Solution | -|-----------|----------| -| **Old windows without commandString** | Reconstruct command from appId + props | -| **Complex req commands** | Intelligently reconstruct all flags from filter object | -| **Hex values** | Encode to npub/note/naddr for readability | -| **Invalid reconstructed command** | User can immediately fix in CommandLauncher | -| **Async commands (NIP-05)** | Full async support maintained | -| **Command changes appId** | Window viewer changes to new app type | -| **Edit mode interrupted** | Edit mode atom automatically cleared on launcher close | - -## Technical Highlights - -### Encoding Strategy - -The reconstructor automatically encodes hex values for better UX: - -```typescript -// Pubkeys → npub -"abc123..." → "npub1..." - -// Event IDs → note -"def456..." → "note1..." - -// Addresses → naddr (for replaceable events) -"30023:pubkey:d-tag" → "naddr1..." -``` - -This makes reconstructed commands readable and matches what users typically type. - -### Req Command Reconstruction - -Most complex reconstruction - handles: -- Kinds: `-k 1,3,7` -- Authors: `-a npub1...,npub2...` (with encoding) -- Limit: `-l 50` -- Tags: `-e`, `-p`, `-t`, `-d`, `--tag` -- Time ranges: `--since`, `--until` -- Search: `--search "text"` -- Flags: `--close-on-eose` -- Relays: `relay1.com relay2.com` - -### State Management Pattern - -Using Jotai atoms for edit mode provides: -- ✅ No prop drilling required -- ✅ Clean separation from main UI state -- ✅ Automatic cleanup on launcher close -- ✅ Type-safe state updates -- ✅ Easy to extend for future features - -## Testing - -### TypeScript Compilation -✅ `npx tsc --noEmit` - No errors - -### Dev Server -✅ Running on http://localhost:5173/ - -### Manual Test Scenarios - -**Test 1: Edit New Window (has commandString)** -1. Create window: `profile alice@domain.com` -2. Click edit button → CommandLauncher opens with "profile alice@domain.com" -3. Change to: `profile bob@domain.com` -4. Window updates to show Bob's profile - -**Test 2: Edit Old Window (no commandString)** -1. Open window from localStorage (created before this feature) -2. Click edit button → Command automatically reconstructed! -3. Edit reconstructed command -4. Window updates and commandString is now saved - -**Test 3: Edit Complex Req Command** -1. Create: `req -k 1,3 -l 20 -t nostr,bitcoin` -2. Click edit → Exact command shown -3. Change to: `req -k 1 -l 50` -4. Window updates with new filter - -**Test 4: Reconstruct and Edit** -1. Old profile window with hex pubkey -2. Click edit → See `profile npub1...` (reconstructed with encoding!) -3. Edit normally -4. Works perfectly - -**Test 5: Change App Type via Edit** -1. Profile window -2. Click edit, change to: `req -k 1` -3. Window changes from ProfileViewer to ReqViewer - -## Performance - -- **Memory**: Minimal (edit mode atom ~100 bytes) -- **Reconstruction**: <1ms for simple commands, <10ms for complex req -- **Encoding**: <1ms per hex value (npub/note encoding) -- **No performance impact**: Only runs when edit button clicked - -## Benefits Over Dialog Approach - -1. **Familiar Interface**: Users already know CommandLauncher -2. **Feature Complete**: All launcher features available (suggestions, hints, validation) -3. **Less Code**: Removed entire EditCommandDialog component -4. **Consistent UX**: Same interface for create and edit -5. **Command History**: Users can use ↑/↓ navigation (already in CommandLauncher) -6. **Visual Feedback**: Parsing hints, command matching, suggestions all work - -## Future Enhancements - -- [ ] Add "(editing)" indicator in CommandLauncher title when in edit mode -- [ ] Command history navigation with ↑/↓ (can leverage existing history feature) -- [ ] Keyboard shortcut: ⌘E to edit focused window -- [ ] Right-click context menu with edit option -- [ ] Undo/Redo system (full Phase 3-5 from design doc) - -## Conclusion - -The final implementation achieves all MVP goals: -- ✅ Edit any window command -- ✅ Reuse familiar CommandLauncher interface -- ✅ Intelligent command reconstruction for old windows -- ✅ Full async support (NIP-05 resolution) -- ✅ Clean architecture with Jotai atoms -- ✅ Type-safe and production-ready - -**Bonus achievements:** -- ✅ Simpler than dialog approach (removed 130 lines of code) -- ✅ Better UX (familiar interface) -- ✅ Smart reconstruction with encoding (npub, note, naddr) -- ✅ Handles all command types including complex req - -The implementation is **production-ready** and provides an excellent user experience by leveraging existing, familiar components! 🎉 diff --git a/claudedocs/IMPLEMENTATION_editable-commands-mvp.md b/claudedocs/IMPLEMENTATION_editable-commands-mvp.md deleted file mode 100644 index e77fd1f..0000000 --- a/claudedocs/IMPLEMENTATION_editable-commands-mvp.md +++ /dev/null @@ -1,223 +0,0 @@ -# Editable Commands MVP - Implementation Summary - -**Date:** 2025-12-13 -**Status:** ✅ Complete -**Complexity:** Medium - -## Overview - -Implemented the MVP for editable window commands, allowing users to edit the command that created a window (e.g., change `profile alice@domain.com` to `profile bob@domain.com`) with full async support and error handling. - -## What Was Implemented - -### Phase 1: Foundation (Completed) - -**Files Modified:** -- `src/types/app.ts` - Added `commandString?: string` to WindowInstance -- `src/core/logic.ts` - Implemented `updateWindow()` pure function and updated `addWindow()` signature -- `src/core/state.ts` - Added `updateWindow` hook and modified `addWindow` to accept commandString parameter - -**Key Features:** -- ✅ Backward compatible: `commandString` is optional, existing windows continue working -- ✅ Pure functional approach: all state mutations immutable -- ✅ Type-safe: full TypeScript coverage - -### Phase 2: Command Parser Utility (Completed) - -**Files Created:** -- `src/lib/command-parser.ts` - Reusable command parsing logic - -**Exports:** -- `parseCommandInput(input)` - Basic command parsing (command name + args) -- `executeCommandParser(parsed)` - Executes argParser with async support -- `parseAndExecuteCommand(input)` - Complete pipeline for command execution - -**Key Features:** -- ✅ DRY principle: single source of truth for parsing -- ✅ Async support: handles NIP-05 resolution and other async operations -- ✅ Error handling: returns structured errors for invalid commands -- ✅ Reusable: used by both CommandLauncher and EditCommandDialog - -### Phase 3: UI Components (Completed) - -**Files Created:** -- `src/components/EditCommandDialog.tsx` - Command editing dialog - -**Files Modified:** -- `src/components/CommandLauncher.tsx` - Now uses command-parser utility and passes commandString -- `src/components/WindowToolbar.tsx` - Added edit button (Pencil icon) and dialog integration -- `src/components/WindowTitle.tsx` - Passes window instance to WindowToolbar - -**UI Features:** -- ✅ Edit button with Pencil icon in window toolbar -- ✅ Modal dialog for command editing -- ✅ Loading states during async parsing (e.g., NIP-05 resolution) -- ✅ Error display without closing dialog -- ✅ Keyboard support (Enter to submit) -- ✅ Fallback message for old windows without commandString -- ✅ Disabled state while loading -- ✅ Input validation (empty command prevention) - -## Technical Highlights - -### Async Command Support - -The implementation fully supports async command parsers: - -```typescript -// Example: profile command with NIP-05 resolution -argParser: async (args: string[]) => { - const parsed = await parseProfileCommand(args); - return parsed; -} -``` - -EditCommandDialog shows "Parsing command..." during async operations. - -### Error Handling - -Comprehensive error handling at multiple levels: -1. **Parse errors**: Unknown command, invalid syntax -2. **Async errors**: NIP-05 resolution failures, network issues -3. **Validation errors**: Empty commands, malformed arguments - -All errors displayed in dialog without closing, allowing user to fix issues. - -### Command String Storage - -Every new window now stores its original command: - -```typescript -// When creating window via CommandLauncher -addWindow(command.appId, props, title, "profile alice@domain.com"); - -// Window object now includes: -{ - id: "uuid", - appId: "profile", - title: "PROFILE alice@domain.com", - props: { pubkey: "..." }, - commandString: "profile alice@domain.com" // NEW -} -``` - -### Window Updates - -Editing a command can change: -- **props**: New data for the window (e.g., different pubkey) -- **title**: Display title updates automatically -- **commandString**: Stores the new command -- **appId**: Can even change the app type (e.g., profile → req) - -```typescript -// User edits: "profile alice" → "req -k 1" -updateWindow(windowId, { - props: { filter: { kinds: [1], limit: 50 } }, - title: "REQ -k 1", - commandString: "req -k 1", - appId: "req" // Window viewer changes completely! -}); -``` - -## Edge Cases Handled - -| Edge Case | Solution | -|-----------|----------| -| **Old windows without commandString** | Show message: "This window was created before command tracking" | -| **Invalid command** | Display error, keep dialog open for fixing | -| **Async parsing** | Show loading state, disable submit during resolution | -| **Empty input** | Disable submit button, show validation error | -| **Command changes appId** | Full window update, viewer changes to new app type | -| **Parsing errors** | Graceful error display with specific error messages | - -## Testing - -### TypeScript Compilation -✅ `npx tsc --noEmit` - No errors - -### Dev Server -✅ `npm run dev` - Running on http://localhost:5173/ - -### Manual Testing Scenarios - -**Test 1: Edit Simple Command** -1. Open window: `nip 01` -2. Click edit button (Pencil icon) -3. Change to: `nip 19` -4. Submit → Window updates to show NIP-19 - -**Test 2: Edit Async Command (NIP-05)** -1. Open window: `profile alice@domain.com` -2. Click edit button -3. Change to: `profile bob@domain.com` -4. See "Parsing command..." loading state -5. Window updates after NIP-05 resolution - -**Test 3: Invalid Command** -1. Open any window -2. Click edit button -3. Enter: `invalidcommand xyz` -4. See error: "Unknown command: invalidcommand" -5. Dialog stays open for correction - -**Test 4: Change App Type** -1. Open window: `profile alice@domain.com` -2. Click edit button -3. Change to: `req -k 1 -l 20` -4. Window completely changes from ProfileViewer to ReqViewer - -**Test 5: Old Window (No commandString)** -1. Use existing window created before this feature -2. Click edit button -3. See message about command tracking -4. Can still enter new command to update window - -## Files Changed Summary - -**Created (2 files):** -- `src/lib/command-parser.ts` -- `src/components/EditCommandDialog.tsx` - -**Modified (6 files):** -- `src/types/app.ts` -- `src/core/logic.ts` -- `src/core/state.ts` -- `src/components/CommandLauncher.tsx` -- `src/components/WindowToolbar.tsx` -- `src/components/WindowTitle.tsx` - -## Future Enhancements (Post-MVP) - -- [ ] Keyboard shortcut: ⌘E to edit focused window -- [ ] Command history navigation: ↑/↓ in edit dialog -- [ ] Undo/Redo system (full Phase 3-5 from design doc) -- [ ] Command validation before showing error (real-time) -- [ ] Command suggestions/autocomplete in edit dialog -- [ ] Right-click context menu with edit option - -## Architecture Benefits - -1. **Clean Separation**: Parser logic separated from UI -2. **Reusability**: Parser used by CommandLauncher and EditCommandDialog -3. **Type Safety**: Full TypeScript coverage -4. **Testability**: Pure functions easy to unit test -5. **Extensibility**: Easy to add command history, undo/redo later -6. **Backward Compatible**: No breaking changes to existing code - -## Performance - -- **Memory**: Minimal (commandString adds ~50-100 bytes per window) -- **Parsing**: <10ms for simple commands, 100-3000ms for async (NIP-05) -- **UI Responsiveness**: Instant dialog open, loading states during async -- **State Updates**: O(1) immutable updates via spread operators - -## Conclusion - -The editable commands MVP is **fully functional and production-ready**. Users can now: -- ✅ Edit any window command -- ✅ Handle async commands (NIP-05) -- ✅ See clear error messages -- ✅ Experience smooth loading states -- ✅ Update window data instantly - -The implementation follows the design document (Phases 1-2), maintains code quality standards, and provides an excellent foundation for future enhancements (history, undo/redo). diff --git a/claudedocs/PLAN-repository-tree-visualization.md b/claudedocs/PLAN-repository-tree-visualization.md deleted file mode 100644 index fa22de6..0000000 --- a/claudedocs/PLAN-repository-tree-visualization.md +++ /dev/null @@ -1,589 +0,0 @@ -# Plan: Repository Tree Visualization for RepositoryDetailRenderer - -## Overview - -Add file tree exploration and file content viewing to the Repository detail renderer (kind 30617), using the `@fiatjaf/git-natural-api` library to fetch git trees via HTTP from clone URLs. - -## Library Analysis: `@fiatjaf/git-natural-api` - -**Package**: `jsr:@fiatjaf/git-natural-api@0.1.3` -**Dependencies**: `@noble/hashes` (SHA-1), `fflate` (compression) - -### Exported Functions - -```typescript -// Get directory tree at a ref (uses blob:none filter - tree structure only, no file contents) -async function getDirectoryTreeAt(url: string, ref: string): Promise - -// Get full tree with file contents (shallow clone) -async function shallowCloneRepositoryAt(url: string, ref: string): Promise - -// Get a single git object by hash -async function getObject(url: string, hash: string): Promise - -// Get info/refs from server (to find default branch, capabilities) -async function getInfoRefs(url: string): Promise - -// Lower-level utilities -function loadTree(treeObject: GitObject, objects: Map): DirectoryTree -function parseTree(data: Uint8Array): TreeEntry[] -``` - -### Return Types (inferred from source) - -```typescript -interface DirectoryTree { - directories: Array<{ - name: string - hash: string - content: DirectoryTree | null // null when using blob:none filter - }> - files: Array<{ - name: string - hash: string - content: Uint8Array | null // null when using blob:none filter - }> -} - -interface InfoRefs { - service: string | null - refs: Record // e.g., {"refs/heads/main": "abc123..."} - capabilities: string[] - symrefs: Record // e.g., {"HEAD": "refs/heads/main"} -} -``` - -### Error Classes - -```typescript -class MissingCapability extends Error { - url: string - capability: string -} - -class MissingRef extends Error {} -``` - -### Required Server Capabilities - -The library requires these git protocol capabilities: -- `multi_ack_detailed` - Required, throws if missing -- `side-band-64k` - Required, throws if missing -- `shallow` - Required -- `object-format=sha1` - Required -- `filter` - Required for `getDirectoryTreeAt` (uses `blob:none`) -- `ofs-delta` - Optional, used if available - -**Important**: Many git servers (especially self-hosted) may not support the `filter` capability. Fallback to `shallowCloneRepositoryAt` is needed. - ---- - -## Implementation Plan - -### Phase 1: Core Infrastructure - -#### 1.1 Add git-natural-api dependency - -```bash -npm install jsr:@fiatjaf/git-natural-api -``` - -Or via import map / esm.sh for JSR packages. - -#### 1.2 Create `useGitTree` hook - -**File**: `src/hooks/useGitTree.ts` - -```typescript -interface UseGitTreeOptions { - cloneUrls: string[] // Try multiple URLs in order - ref?: string // Branch/tag/commit, defaults to HEAD -} - -interface UseGitTreeResult { - tree: DirectoryTree | null - loading: boolean - error: Error | null - serverUrl: string | null // Which server succeeded - refetch: () => void -} -``` - -**Logic**: -1. Try each clone URL in sequence -2. First call `getInfoRefs` to check capabilities and resolve ref -3. If `filter` capability exists, use `getDirectoryTreeAt` (lighter) -4. Otherwise fall back to `shallowCloneRepositoryAt` -5. Cache result (possibly in Dexie for offline access) -6. Handle `MissingCapability`, `MissingRef` errors gracefully - -#### 1.3 Create `useGitBlob` hook for file content - -**File**: `src/hooks/useGitBlob.ts` - -```typescript -interface UseGitBlobOptions { - serverUrl: string - hash: string -} - -interface UseGitBlobResult { - content: Uint8Array | null - loading: boolean - error: Error | null -} -``` - -Use `getObject(url, hash)` to fetch individual file blobs on demand. - ---- - -### Phase 2: File Tree Component - -#### 2.1 Create `FileTreeView` component - -**File**: `src/components/ui/FileTreeView.tsx` - -Design goals: -- Match Grimoire's dark aesthetic with existing UI primitives -- Use Radix Collapsible/Accordion for expand/collapse -- Lucide icons: `Folder`, `FolderOpen`, `File`, `FileCode`, `FileText`, etc. -- Support keyboard navigation (arrow keys, enter to expand/select) - -```typescript -interface FileTreeViewProps { - tree: DirectoryTree - onFileSelect: (file: { name: string; hash: string; path: string }) => void - selectedPath?: string - className?: string -} -``` - -**Structure**: -- Recursive component for directories -- File icons based on extension (map common extensions to icons) -- Alphabetical sorting (directories first) -- Expandable directory nodes with chevron -- Click file to select → triggers file content fetch - -#### 2.2 File Icon Mapping - -```typescript -const fileIcons: Record = { - // Code files - '.ts': FileCode, '.tsx': FileCode, '.js': FileCode, '.jsx': FileCode, - '.py': FileCode, '.rs': FileCode, '.go': FileCode, '.rb': FileCode, - // Config/data - '.json': FileJson, '.yaml': FileText, '.yml': FileText, '.toml': FileText, - // Documentation - '.md': FileText, '.txt': FileText, '.rst': FileText, - // Default - 'default': File, -} -``` - ---- - -### Phase 3: Syntax Highlighting (Lazy Loading) - -#### 3.1 Migrate to Shiki with on-demand loading - -**Current state**: Using Prism.js with statically imported languages (diff, js, ts, jsx, tsx, bash, json, markdown, css, python, yaml). - -**Problem**: Loading all highlighters upfront is wasteful for file tree where we need many more languages. - -**Solution**: Use Shiki with fine-grained bundles and lazy loading. - -**File**: `src/lib/shiki.ts` - -```typescript -import { createHighlighterCore, type HighlighterCore } from 'shiki/core' -import { createOnigurumaEngine } from 'shiki/engine/oniguruma' - -let highlighter: HighlighterCore | null = null -const loadedLanguages = new Set() -const loadedThemes = new Set() - -// Language to Shiki language ID mapping -const languageMap: Record = { - 'ts': 'typescript', - 'tsx': 'tsx', - 'js': 'javascript', - 'jsx': 'jsx', - 'py': 'python', - 'rs': 'rust', - 'go': 'go', - 'rb': 'ruby', - 'json': 'json', - 'yaml': 'yaml', - 'yml': 'yaml', - 'md': 'markdown', - 'css': 'css', - 'html': 'html', - 'sh': 'bash', - 'bash': 'bash', - 'diff': 'diff', - // ... more mappings -} - -export async function getHighlighter(): Promise { - if (!highlighter) { - highlighter = await createHighlighterCore({ - themes: [import('@shikijs/themes/github-dark')], - langs: [], // Start empty, load on demand - engine: createOnigurumaEngine(import('shiki/wasm')) - }) - loadedThemes.add('github-dark') - } - return highlighter -} - -export async function highlightCode(code: string, ext: string): Promise { - const lang = languageMap[ext.replace('.', '')] || 'text' - const hl = await getHighlighter() - - // Load language if not already loaded - if (!loadedLanguages.has(lang) && lang !== 'text') { - try { - await hl.loadLanguage(import(`@shikijs/langs/${lang}`)) - loadedLanguages.add(lang) - } catch { - // Language not available, fall back to plaintext - } - } - - return hl.codeToHtml(code, { lang: loadedLanguages.has(lang) ? lang : 'text', theme: 'github-dark' }) -} -``` - -#### 3.2 Create `LazyCodeViewer` component - -**File**: `src/components/ui/LazyCodeViewer.tsx` - -```typescript -interface LazyCodeViewerProps { - content: Uint8Array | string - filename: string - className?: string -} - -export function LazyCodeViewer({ content, filename, className }: LazyCodeViewerProps) { - const [html, setHtml] = useState(null) - const [loading, setLoading] = useState(true) - const ext = getFileExtension(filename) - - useEffect(() => { - const text = typeof content === 'string' - ? content - : new TextDecoder().decode(content) - - highlightCode(text, ext) - .then(setHtml) - .finally(() => setLoading(false)) - }, [content, ext]) - - if (loading) return - - return ( -
- ) -} -``` - -#### 3.3 Theme Integration - -Create a Shiki theme that matches Grimoire's CSS variables, or use a compatible preset (github-dark, one-dark-pro) and apply custom CSS overrides. - ---- - -### Phase 4: RepositoryDetailRenderer Integration - -#### 4.1 Add Tree Section to Detail Renderer - -**File**: `src/components/nostr/kinds/RepositoryDetailRenderer.tsx` - -Add new "Files" section after URLs/Maintainers: - -```tsx -// In RepositoryDetailRenderer -const cloneUrls = useMemo(() => getCloneUrls(event), [event]) - -const { tree, loading, error, serverUrl } = useGitTree({ - cloneUrls, - ref: 'HEAD' // or allow branch selection later -}) - -return ( -
- {/* ... existing sections ... */} - - {/* Files Section */} - {cloneUrls.length > 0 && ( - - )} -
-) -``` - -#### 4.2 Create `RepositoryFilesSection` component - -**File**: `src/components/nostr/kinds/RepositoryFilesSection.tsx` - -```tsx -interface RepositoryFilesSectionProps { - cloneUrls: string[] - tree: DirectoryTree | null - loading: boolean - error: Error | null - serverUrl: string | null -} - -export function RepositoryFilesSection({ - cloneUrls, - tree, - loading, - error, - serverUrl -}: RepositoryFilesSectionProps) { - const [selectedFile, setSelectedFile] = useState<{name: string, hash: string, path: string} | null>(null) - - // Fetch file content when selected - const { content, loading: contentLoading } = useGitBlob({ - serverUrl, - hash: selectedFile?.hash - }) - - if (loading) { - return ( -
-

- - Files -

- -
- ) - } - - if (error) { - return ( -
-

- - Files -

-
-

Unable to load repository files.

-

{error.message}

-
-
- ) - } - - if (!tree) return null - - return ( -
-

- - Files - {serverUrl && ( - - from {new URL(serverUrl).hostname} - - )} -

- -
- {/* File Tree */} -
- -
- - {/* File Content Preview */} -
- {selectedFile ? ( - contentLoading ? ( - - ) : content ? ( - - ) : ( -
- Unable to load file content -
- ) - ) : ( -
- Select a file to view its contents -
- )} -
-
-
- ) -} -``` - ---- - -### Phase 5: Resilience & Caching - -#### 5.1 Multi-Server Fallback - -Repositories have multiple clone URLs. Try each in order: - -```typescript -async function tryFetchTree(cloneUrls: string[], ref: string): Promise<{ - tree: DirectoryTree - serverUrl: string -}> { - const errors: Error[] = [] - - for (const url of cloneUrls) { - try { - // Check capabilities first - const info = await getInfoRefs(url) - const hasFilter = info.capabilities.includes('filter') - - // Resolve ref if symbolic - const resolvedRef = ref.startsWith('refs/') - ? info.refs[ref] - : info.symrefs['HEAD']?.startsWith('refs/') - ? info.refs[info.symrefs['HEAD']] - : ref - - const tree = hasFilter - ? await getDirectoryTreeAt(url, resolvedRef) - : await shallowCloneRepositoryAt(url, resolvedRef) - - return { tree, serverUrl: url } - } catch (e) { - errors.push(e as Error) - continue - } - } - - throw new AggregateError(errors, 'All servers failed') -} -``` - -#### 5.2 Dexie Caching - -Cache trees and blobs in IndexedDB for offline access: - -```typescript -// In src/services/db.ts -interface GitTreeCache { - id: string // `${serverUrl}:${ref}` - tree: DirectoryTree - fetchedAt: number -} - -interface GitBlobCache { - hash: string - content: Uint8Array - fetchedAt: number -} - -// Add to Dexie schema -db.version(X).stores({ - // ... existing stores ... - gitTrees: 'id, fetchedAt', - gitBlobs: 'hash, fetchedAt' -}) -``` - -TTL: 1 hour for trees, indefinite for blobs (content-addressed). - ---- - -## File Tree UI Options - -Given Grimoire's aesthetic (dark theme, Tailwind, Radix primitives), we should build a custom component rather than import a library. Options considered: - -1. **Custom with Radix Collapsible** (Recommended) - - Full control over styling - - Matches existing app patterns - - Uses existing dependencies - -2. **FlyonUI Tree View** - - Tailwind-based - - Would require adding Preline plugins - - Overkill for our needs - -3. **react-arborist** - - Full-featured tree with virtualization - - Heavy dependency - - More than we need - -**Recommendation**: Build custom using Radix Collapsible + Lucide icons, following patterns from existing Accordion component. - ---- - -## Bundle Size Considerations - -### Current Prism Setup -- `prismjs` core: ~20KB -- Each language: 1-5KB -- Currently importing 11 languages statically - -### Shiki Migration -- `shiki/core`: ~15KB -- Each language: 10-50KB (grammars are larger but more accurate) -- WASM engine: ~200KB (one-time) -- Themes: 2-5KB each - -**Strategy**: -- Keep Prism for existing uses (diff highlighting in patches) -- Use Shiki only for the file viewer with lazy loading -- Or fully migrate to Shiki with lazy loading everywhere - -**Recommendation**: Full Shiki migration for consistency, using lazy loading for all languages. - ---- - -## Implementation Order - -1. **Core hooks** (`useGitTree`, `useGitBlob`) -2. **FileTreeView component** (minimal, collapsible tree) -3. **RepositoryFilesSection** integration -4. **Shiki migration** with lazy loading -5. **LazyCodeViewer** component -6. **Caching layer** in Dexie -7. **Polish** (loading states, error handling, keyboard nav) - ---- - -## Testing Strategy - -- Unit tests for file extension → language mapping -- Unit tests for tree traversal/sorting -- Integration tests with mock git server responses -- Manual testing with various repository types (GitHub, GitLab, self-hosted) - ---- - -## Known Limitations - -1. **Filter capability**: Many self-hosted git servers don't support `blob:none` filter. Fallback to full shallow clone works but is heavier. - -2. **Large repositories**: Tree can be huge for monorepos. Consider pagination/virtualization for 1000+ files. - -3. **Binary files**: Need detection and handling (show "Binary file" instead of trying to decode). - -4. **Private repositories**: The API works with public repos only. Would need auth token support for private. - -5. **CORS**: Some git servers may not allow browser requests. May need a proxy or show a helpful error. diff --git a/claudedocs/PLAN-shiki-migration.md b/claudedocs/PLAN-shiki-migration.md deleted file mode 100644 index d891742..0000000 --- a/claudedocs/PLAN-shiki-migration.md +++ /dev/null @@ -1,699 +0,0 @@ -# Plan: Migrate from Prism.js to Shiki with Lazy Loading - -## Overview - -Replace the current Prism.js-based syntax highlighting with Shiki, enabling on-demand language loading while preserving the current minimalistic styling. - ---- - -## Current State Analysis - -### Files Using `SyntaxHighlight` - -| File | Language(s) Used | Context | -|------|------------------|---------| -| `PatchDetailRenderer.tsx` | `diff` | Git patches | -| `CodeSnippetRenderer.tsx` | dynamic (via `mapLanguage()`) | NIP-C0 code snippets | -| `CodeSnippetDetailRenderer.tsx` | dynamic (via `normalizedLanguage`) | Code snippet detail view | -| `MarkdownContent.tsx` | dynamic (from markdown) | Article code blocks | -| `JsonEventRow.tsx` | `json` | Event JSON view | -| `DebugViewer.tsx` | `json` | App state debug | -| `JsonViewer.tsx` | `json` | JSON dialog | -| `ReqViewer.tsx` | `json` | Filter JSON display | - -### Current Prism Setup - -**Statically loaded languages** (`src/components/SyntaxHighlight.tsx`): -- diff, javascript, typescript, jsx, tsx -- bash, json, markdown, css, python, yaml - -**Theme**: Custom CSS in `src/styles/prism-theme.css` using CSS variables: -- Uses `hsl(var(--foreground))`, `hsl(var(--muted-foreground))`, `hsl(var(--primary))` -- Special handling for diff tokens (deleted, inserted, coord) -- Minimal color palette - mostly foreground/muted with primary accents - -### Problems with Current Approach - -1. **Bundle bloat**: All 11 language grammars loaded upfront (~30KB) -2. **Limited languages**: Markdown code blocks with unsupported languages show no highlighting -3. **No extension path**: Adding new languages requires editing component imports -4. **Prism limitations**: Less accurate grammars compared to TextMate (used by VS Code/Shiki) - ---- - -## Shiki Benefits - -1. **TextMate grammars**: Same grammars as VS Code, more accurate highlighting -2. **Lazy loading**: Languages loaded on-demand via dynamic imports -3. **Fine-grained bundles**: Control exactly what's bundled -4. **Theme flexibility**: CSS variables or inline styles -5. **200+ languages**: Support virtually any language users might use - ---- - -## Migration Plan - -### Phase 1: Core Infrastructure - -#### 1.1 Install Dependencies - -```bash -npm install shiki -``` - -**Bundle impact**: -- `shiki/core`: ~15KB (min+gzip) -- Oniguruma WASM: ~200KB (one-time, cached) -- Languages: 10-50KB each (lazy loaded) -- Theme: 2-5KB - -#### 1.2 Create Shiki Service - -**File**: `src/lib/shiki.ts` - -```typescript -import { createHighlighterCore, type HighlighterCore } from 'shiki/core' -import { createOnigurumaEngine } from 'shiki/engine/oniguruma' - -// Singleton highlighter instance -let highlighter: HighlighterCore | null = null -let highlighterPromise: Promise | null = null -const loadedLanguages = new Set() - -// Grimoire dark theme matching current Prism styles -const grimoireTheme = { - name: 'grimoire-dark', - type: 'dark' as const, - colors: { - 'editor.background': '#000000', - 'editor.foreground': '#e5e5e5', - }, - tokenColors: [ - // Comments - muted - { - scope: ['comment', 'punctuation.definition.comment'], - settings: { foreground: '#6b7280' } // muted-foreground - }, - // Strings - muted but slightly emphasized - { - scope: ['string', 'string.quoted'], - settings: { foreground: '#9ca3af', fontStyle: '' } - }, - // Keywords, operators - primary color - { - scope: ['keyword', 'storage', 'keyword.operator'], - settings: { foreground: '#a855f7' } // primary purple - }, - // Functions, classes - primary bold - { - scope: ['entity.name.function', 'entity.name.class', 'entity.name.type'], - settings: { foreground: '#a855f7', fontStyle: 'bold' } - }, - // Numbers, constants - primary - { - scope: ['constant', 'constant.numeric', 'constant.language'], - settings: { foreground: '#a855f7' } - }, - // Variables, parameters - foreground - { - scope: ['variable', 'variable.parameter'], - settings: { foreground: '#e5e5e5' } - }, - // Punctuation - slightly muted - { - scope: ['punctuation'], - settings: { foreground: '#b3b3b3' } - }, - // Properties, attributes - { - scope: ['variable.other.property', 'entity.other.attribute-name'], - settings: { foreground: '#d4d4d4' } - }, - // Tags (HTML/JSX) - { - scope: ['entity.name.tag'], - settings: { foreground: '#a855f7' } - }, - // Diff - deleted (red) - { - scope: ['markup.deleted', 'punctuation.definition.deleted'], - settings: { foreground: '#ff8787' } - }, - // Diff - inserted (green) - { - scope: ['markup.inserted', 'punctuation.definition.inserted'], - settings: { foreground: '#69db7c' } - }, - // Diff - changed - { - scope: ['markup.changed'], - settings: { foreground: '#66d9ef' } - }, - ] -} - -/** - * Language alias mapping (file extensions and common names to Shiki IDs) - */ -const LANGUAGE_ALIASES: Record = { - // Extensions - 'ts': 'typescript', - 'tsx': 'tsx', - 'js': 'javascript', - 'jsx': 'jsx', - 'py': 'python', - 'rb': 'ruby', - 'rs': 'rust', - 'go': 'go', - 'sh': 'bash', - 'bash': 'bash', - 'shell': 'bash', - 'zsh': 'bash', - 'yml': 'yaml', - 'yaml': 'yaml', - 'md': 'markdown', - 'json': 'json', - 'jsonc': 'jsonc', - 'css': 'css', - 'scss': 'scss', - 'sass': 'sass', - 'less': 'less', - 'html': 'html', - 'htm': 'html', - 'xml': 'xml', - 'svg': 'xml', - 'sql': 'sql', - 'c': 'c', - 'cpp': 'cpp', - 'c++': 'cpp', - 'h': 'c', - 'hpp': 'cpp', - 'cs': 'csharp', - 'java': 'java', - 'kt': 'kotlin', - 'swift': 'swift', - 'php': 'php', - 'lua': 'lua', - 'vim': 'viml', - 'toml': 'toml', - 'ini': 'ini', - 'dockerfile': 'dockerfile', - 'docker': 'dockerfile', - 'makefile': 'makefile', - 'make': 'makefile', - 'diff': 'diff', - 'patch': 'diff', - 'sol': 'solidity', - 'zig': 'zig', - 'ex': 'elixir', - 'exs': 'elixir', - 'erl': 'erlang', - 'hs': 'haskell', - 'ml': 'ocaml', - 'clj': 'clojure', - 'scala': 'scala', - 'graphql': 'graphql', - 'gql': 'graphql', - 'proto': 'protobuf', - 'nix': 'nix', -} - -/** - * Core languages to preload (most commonly used) - */ -const CORE_LANGUAGES = ['javascript', 'typescript', 'json', 'diff', 'bash'] - -/** - * Normalize language identifier to Shiki language ID - */ -export function normalizeLanguage(lang: string | null | undefined): string { - if (!lang) return 'text' - const normalized = lang.toLowerCase().trim() - return LANGUAGE_ALIASES[normalized] || normalized -} - -/** - * Get or create the singleton highlighter instance - */ -export async function getHighlighter(): Promise { - if (highlighter) return highlighter - - if (!highlighterPromise) { - highlighterPromise = createHighlighterCore({ - themes: [grimoireTheme], - langs: CORE_LANGUAGES.map(lang => import(`shiki/langs/${lang}.mjs`)), - engine: createOnigurumaEngine(import('shiki/wasm')) - }).then(hl => { - highlighter = hl - CORE_LANGUAGES.forEach(l => loadedLanguages.add(l)) - return hl - }) - } - - return highlighterPromise -} - -/** - * Load a language on demand - */ -async function loadLanguage(lang: string): Promise { - if (lang === 'text' || loadedLanguages.has(lang)) return true - - const hl = await getHighlighter() - - try { - // Dynamic import for the language - await hl.loadLanguage(import(`shiki/langs/${lang}.mjs`)) - loadedLanguages.add(lang) - return true - } catch (e) { - console.warn(`[shiki] Language "${lang}" not available, falling back to plaintext`) - return false - } -} - -/** - * Highlight code with lazy language loading - * Returns HTML string - */ -export async function highlightCode( - code: string, - language: string | null | undefined -): Promise { - const lang = normalizeLanguage(language) - const hl = await getHighlighter() - - // Try to load the language if not already loaded - const loaded = await loadLanguage(lang) - const effectiveLang = loaded ? lang : 'text' - - return hl.codeToHtml(code, { - lang: effectiveLang, - theme: 'grimoire-dark' - }) -} - -/** - * Check if a language is available (loaded or loadable) - */ -export function isLanguageLoaded(lang: string): boolean { - return loadedLanguages.has(normalizeLanguage(lang)) -} - -/** - * Preload languages (e.g., on app startup or before rendering) - */ -export async function preloadLanguages(langs: string[]): Promise { - await getHighlighter() - await Promise.all(langs.map(l => loadLanguage(normalizeLanguage(l)))) -} -``` - -#### 1.3 Create React Hook - -**File**: `src/hooks/useHighlightedCode.ts` - -```typescript -import { useState, useEffect } from 'react' -import { highlightCode, normalizeLanguage } from '@/lib/shiki' - -interface UseHighlightedCodeResult { - html: string | null - loading: boolean - error: Error | null -} - -/** - * Hook to highlight code asynchronously with lazy language loading - */ -export function useHighlightedCode( - code: string, - language: string | null | undefined -): UseHighlightedCodeResult { - const [html, setHtml] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - let cancelled = false - setLoading(true) - setError(null) - - highlightCode(code, language) - .then(result => { - if (!cancelled) { - setHtml(result) - setLoading(false) - } - }) - .catch(err => { - if (!cancelled) { - setError(err) - setLoading(false) - } - }) - - return () => { - cancelled = true - } - }, [code, language]) - - return { html, loading, error } -} -``` - ---- - -### Phase 2: New SyntaxHighlight Component - -#### 2.1 Replace SyntaxHighlight Component - -**File**: `src/components/SyntaxHighlight.tsx` (rewrite) - -```typescript -import { useHighlightedCode } from '@/hooks/useHighlightedCode' -import { cn } from '@/lib/utils' - -interface SyntaxHighlightProps { - code: string - language?: string | null - className?: string - showLineNumbers?: boolean -} - -/** - * Syntax highlighting component using Shiki with lazy language loading - * - * @example - * - * - */ -export function SyntaxHighlight({ - code, - language, - className = '', - showLineNumbers = false, -}: SyntaxHighlightProps) { - const { html, loading, error } = useHighlightedCode(code, language) - - // Loading state - show code without highlighting - if (loading) { - return ( -
-        {code}
-      
- ) - } - - // Error state - fallback to plain code - if (error || !html) { - return ( -
-        {code}
-      
- ) - } - - // Render highlighted HTML - return ( -
- ) -} -``` - ---- - -### Phase 3: CSS Theme Migration - -#### 3.1 Update Styles - -**File**: `src/styles/shiki-theme.css` (new file, replaces prism-theme.css) - -```css -/* Shiki syntax highlighting - Grimoire dark theme overrides */ - -/* Base container styling */ -.shiki-container pre { - background: transparent !important; - margin: 0; - padding: 0; -} - -.shiki-container code { - font-family: "Oxygen Mono", monospace; - font-size: 0.75rem; - line-height: 1.5; - white-space: pre; - word-spacing: normal; - word-break: normal; - tab-size: 4; -} - -/* Loading state - subtle pulse animation */ -.shiki-loading { - animation: shiki-pulse 1.5s ease-in-out infinite; -} - -@keyframes shiki-pulse { - 0%, 100% { opacity: 0.7; } - 50% { opacity: 0.5; } -} - -/* Diff-specific styling - block-level backgrounds for inserted/deleted */ -.shiki-container .line.diff.add { - background: rgba(52, 199, 89, 0.1); - display: block; - margin: 0 -1rem; - padding: 0 1rem; -} - -.shiki-container .line.diff.remove { - background: rgba(255, 59, 48, 0.1); - display: block; - margin: 0 -1rem; - padding: 0 1rem; -} - -/* Hunk headers (@@ lines) */ -.shiki-container .line.diff.info { - background: rgba(102, 217, 239, 0.08); - display: block; - margin: 0 -1rem; - padding: 0 1rem; - font-weight: 600; -} - -/* Optional: Line numbers */ -.shiki-container.line-numbers .line::before { - content: attr(data-line); - display: inline-block; - width: 2rem; - margin-right: 1rem; - text-align: right; - color: hsl(var(--muted-foreground)); - border-right: 1px solid hsl(var(--border)); - padding-right: 0.5rem; -} -``` - -#### 3.2 Update index.css - -**File**: `src/index.css` (modify) - -```css -/* Replace Prism import with Shiki */ -/* @import "./styles/prism-theme.css"; */ /* Remove */ -@import "./styles/shiki-theme.css"; /* Add */ -``` - ---- - -### Phase 4: Component Updates - -#### 4.1 Update Consumers (Minimal Changes) - -Most components need no changes since the `SyntaxHighlight` interface remains the same: - -```typescript -// Before (Prism) - - -// After (Shiki) - same API! - -``` - -#### 4.2 Update MarkdownContent for Async Highlighting - -The `CodeBlock` component in `MarkdownContent.tsx` needs slight adjustment to handle the async nature: - -```typescript -// In MarkdownContent.tsx, update CodeBlock: -function CodeBlock({ code, language }: { code: string; language: string | null }) { - const { copy, copied } = useCopy() - const isSingleLine = !code.includes('\n') - - return ( -
- - {!isSingleLine && ( - copy(code)} copied={copied} /> - )} -
- ) -} -``` - -The `language={language as any}` casts can be removed since Shiki accepts any string. - -#### 4.3 Update CodeSnippetRenderer Language Mapping - -Simplify since Shiki handles more languages: - -```typescript -// Before: Complex mapLanguage with limited support -function mapLanguage(lang: string | null | undefined): "javascript" | ... { } - -// After: Just normalize, Shiki handles the rest -import { normalizeLanguage } from '@/lib/shiki' -// Use normalizeLanguage(language) - returns valid language or 'text' -``` - -#### 4.4 Remove Prism-specific Code - -In `CodeSnippetDetailRenderer.tsx`, simplify the language validation: - -```typescript -// Before: Manual list of supported languages -const supported = ['javascript', 'typescript', ...] -return supported.includes(mapped) ? mapped : null - -// After: Let Shiki handle it -const normalizedLanguage = normalizeLanguage(language) -// Always use it - Shiki will fall back to plaintext if unknown -``` - ---- - -### Phase 5: Cleanup - -#### 5.1 Remove Prism Dependencies - -```bash -npm uninstall prismjs @types/prismjs -``` - -#### 5.2 Delete Old Files - -- `src/styles/prism-theme.css` → deleted (replaced by shiki-theme.css) - -#### 5.3 Remove Prism Imports - -Search and remove all: -```typescript -import Prism from "prismjs" -import "prismjs/components/prism-*" -``` - ---- - -## Migration Summary - -### Files to Create -| File | Purpose | -|------|---------| -| `src/lib/shiki.ts` | Shiki service with lazy loading | -| `src/hooks/useHighlightedCode.ts` | React hook for async highlighting | -| `src/styles/shiki-theme.css` | Theme styles | - -### Files to Modify -| File | Changes | -|------|---------| -| `src/components/SyntaxHighlight.tsx` | Complete rewrite | -| `src/index.css` | Replace Prism import with Shiki | -| `src/components/nostr/MarkdownContent.tsx` | Remove `as any` casts | -| `src/components/nostr/kinds/CodeSnippetRenderer.tsx` | Simplify mapLanguage | -| `src/components/nostr/kinds/CodeSnippetDetailRenderer.tsx` | Remove supported list | - -### Files to Delete -| File | Reason | -|------|--------| -| `src/styles/prism-theme.css` | Replaced by shiki-theme.css | - -### Dependencies -| Remove | Add | -|--------|-----| -| `prismjs` | `shiki` | -| `@types/prismjs` | - | - ---- - -## Testing Checklist - -- [ ] **JSON highlighting**: DebugViewer, JsonViewer, JsonEventRow, ReqViewer -- [ ] **Diff highlighting**: PatchDetailRenderer (git patches) -- [ ] **Code snippets**: CodeSnippetRenderer, CodeSnippetDetailRenderer -- [ ] **Markdown code blocks**: MarkdownContent (articles, NIPs) - - [ ] JavaScript/TypeScript blocks - - [ ] Python blocks - - [ ] Unknown language blocks (should fallback gracefully) -- [ ] **Loading states**: Check flash/pulse during language loading -- [ ] **Bundle size**: Verify lazy loading works (check network tab) -- [ ] **Theme consistency**: Colors match current Prism theme - ---- - -## Performance Considerations - -### Initial Load -- Core languages (JS, TS, JSON, diff, bash) preloaded -- WASM engine loaded once, cached by browser -- Other languages loaded on first use - -### Caching -- Shiki caches compiled grammars in memory -- WASM binary cached by browser (~200KB, infrequent) -- Language grammars cached per session - -### Optimization Tips -1. Preload languages for known content types -2. Use `loading` state to show code immediately (unhighlighted) -3. Consider debouncing for rapidly changing code - ---- - -## Rollback Plan - -If issues arise: -1. Revert `package.json` to restore prismjs -2. Restore `prism-theme.css` -3. Restore original `SyntaxHighlight.tsx` -4. Revert `index.css` import - -The old Prism setup is self-contained and easy to restore. - ---- - -## Future Enhancements - -1. **Line highlighting**: Add ability to highlight specific lines -2. **Copy line numbers**: Option to include line numbers in copy -3. **Language detection**: Auto-detect language from content -4. **Custom themes**: Allow user theme selection -5. **Repository file viewer**: Extend for git tree visualization diff --git a/claudedocs/THEME_TOKEN_SYSTEM.md b/claudedocs/THEME_TOKEN_SYSTEM.md deleted file mode 100644 index 99ed587..0000000 --- a/claudedocs/THEME_TOKEN_SYSTEM.md +++ /dev/null @@ -1,812 +0,0 @@ -# Grimoire Theme Token System - -## Overview - -Comprehensive design token system for Grimoire's visual styling. This system abstracts all visual properties into semantic tokens that can be applied to any theme (dark, light, or custom). Built on industry-standard design token patterns used by Tailwind, Radix, and Material Design. - -**Status**: Design Complete - Ready for Implementation -**Approach**: Two-tier token system (Base + Semantic) -**Format**: HSL for colors, CSS length units for spacing/sizing -**Philosophy**: Theme-agnostic components, theme-specific values - ---- - -## Token Categories - -### 1. Colors (Surfaces & Interactive) -Semantic color tokens for backgrounds, text, and UI elements. - -**Structure**: -```typescript -colors: { - // Surfaces - background: HSL; // Main app background - foreground: HSL; // Main text color - card: HSL; // Elevated surface - "card-foreground": HSL; - popover: HSL; // Floating elements - "popover-foreground": HSL; - - // Interactive Elements - primary: HSL; // Primary actions (buttons, links) - "primary-foreground": HSL; - secondary: HSL; // Secondary actions - "secondary-foreground": HSL; - muted: HSL; // Subtle backgrounds - "muted-foreground": HSL; // Secondary text - accent: HSL; // Highlight/emphasis (purple!) - "accent-foreground": HSL; - destructive: HSL; // Errors, danger - "destructive-foreground": HSL; - - // UI Elements - border: HSL; // Dividers, borders - input: HSL; // Input backgrounds - ring: HSL; // Focus indicators -} -``` - -**Current Dark Theme Values**: -```typescript -colors: { - background: "222.2 84% 4.9%", // Deep blue-gray - foreground: "210 40% 98%", // Almost white - accent: "270 100% 70%", // Grimoire purple! - // ... full definition in grimoire-dark.ts -} -``` - -**Light Theme Values**: -```typescript -colors: { - background: "40 20% 97%", // Warm off-white - foreground: "222 84% 8%", // Very dark - accent: "270 100% 50%", // Darker purple (contrast) - // ... full definition in grimoire-light.ts -} -``` - ---- - -### 2. Gradients -Multi-stop gradient definitions for brand and decorative purposes. - -**Structure**: -```typescript -gradients: { - brand: [HSL, HSL, HSL, HSL]; // Grimoire 4-color brand gradient -} -``` - -**Current Values**: -```typescript -// Dark theme -gradients: { - brand: [ - "43 100% 54%", // Yellow - "25 95% 61%", // Orange - "270 91% 65%", // Purple - "188 86% 53%", // Cyan - ], -} - -// Light theme (adjusted for contrast) -gradients: { - brand: [ - "43 100% 45%", // Darker yellow - "25 95% 50%", // Darker orange - "270 91% 50%", // Darker purple - "188 86% 40%", // Darker cyan - ], -} -``` - -**Usage**: -```css -.text-grimoire-gradient { - background: linear-gradient( - to bottom, - hsl(var(--gradient-brand-1)), - hsl(var(--gradient-brand-2)), - hsl(var(--gradient-brand-3)), - hsl(var(--gradient-brand-4)) - ); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} -``` - ---- - -### 3. Syntax Highlighting -Code syntax highlighting colors for Prism.js integration. - -**Structure**: -```typescript -syntax: { - // General syntax tokens - comment: HSL; - keyword: HSL; - string: HSL; - function: HSL; - number: HSL; - operator: HSL; - "operator-opacity"?: Opacity; - - // Diff-specific tokens (git diffs in code blocks) - deleted: HSL; // Removed lines - "deleted-bg": HSL; - "deleted-bg-opacity": Opacity; - - inserted: HSL; // Added lines - "inserted-bg": HSL; - "inserted-bg-opacity": Opacity; - - coord: HSL; // Hunk headers (@@ -1,5 +1,7 @@) - "coord-bg": HSL; - "coord-bg-opacity": Opacity; -} -``` - -**Current Values**: -```typescript -// Dark theme -syntax: { - comment: "215 20.2% 70%", // Medium gray - keyword: "210 40% 98%", // Bright - deleted: "0 70% 75%", // Light red - "deleted-bg": "0 70% 60%", - "deleted-bg-opacity": 0.1, - // ... etc -} - -// Light theme (inverted lightness) -syntax: { - comment: "222 30% 35%", // Dark gray - keyword: "222 47% 11%", // Very dark - deleted: "0 70% 40%", // Dark red - "deleted-bg": "0 70% 60%", - "deleted-bg-opacity": 0.1, - // ... etc -} -``` - -**Usage**: -```css -.token.deleted { - color: hsl(var(--syntax-deleted)); - background: hsl(var(--syntax-deleted-bg) / var(--syntax-deleted-bg-opacity)); -} -``` - ---- - -### 4. Effects & Transparency -Visual effects like shadows, overlays, and opacity values. - -**Structure**: -```typescript -effects: { - "scrollbar-opacity": Opacity; // Scrollbar thumb opacity - "scrollbar-hover-opacity": Opacity; // Hover state - "overlay-opacity": Opacity; // Modal/fullscreen overlays - "preview-opacity": Opacity; // Mosaic drag preview - "shadow-color": HSL; // Shadow base color - "shadow-blur": CSSLength; // Shadow blur radius -} -``` - -**Current Values** (theme-agnostic): -```typescript -effects: { - "scrollbar-opacity": 0.2, - "scrollbar-hover-opacity": 0.3, - "overlay-opacity": 0.92, - "preview-opacity": 0.3, - "shadow-color": "0 0% 0%", // Black for shadows - "shadow-blur": "40px", -} -``` - -**Usage**: -```css -*::-webkit-scrollbar-thumb { - background-color: hsl(var(--foreground) / var(--scrollbar-opacity)); -} - -*::-webkit-scrollbar-thumb:hover { - background-color: hsl(var(--foreground) / var(--scrollbar-hover-opacity)); -} - -[data-rmiz-modal-overlay] { - background-color: hsl(var(--background) / var(--overlay-opacity)) !important; -} -``` - ---- - -### 5. Spacing & Sizing -Layout dimensions and component sizes. - -**Structure**: -```typescript -spacing: { - "toolbar-height": CSSLength; // Mosaic window toolbar - "split-width": CSSLength; // Window split handle - "scrollbar-width": CSSLength; // Scrollbar thickness -} -``` - -**Current Values** (theme-agnostic): -```typescript -spacing: { - "toolbar-height": "30px", - "split-width": "4px", - "scrollbar-width": "8px", -} -``` - -**Usage**: -```css -.mosaic-window .mosaic-window-toolbar { - height: var(--toolbar-height); -} - -.mosaic-split.-row { - width: var(--split-width); -} -``` - ---- - -### 6. Typography -Font families, sizes, and line heights. - -**Structure**: -```typescript -typography: { - "font-family-mono": string; // Monospace font stack - "font-size-base": CSSLength; // Base text size - "font-size-code": CSSLength; // Code block text size - "line-height-base": string; // Base line height - "line-height-code": string; // Code block line height -} -``` - -**Current Values** (theme-agnostic): -```typescript -typography: { - "font-family-mono": "'Oxygen Mono', monospace", - "font-size-base": "1rem", - "font-size-code": "0.75rem", - "line-height-base": "1.5", - "line-height-code": "1.5", -} -``` - -**Usage**: -```css -body { - font-family: var(--font-mono); - font-size: var(--font-size-base); - line-height: var(--line-height-base); -} - -code { - font-size: var(--font-size-code); - line-height: var(--line-height-code); -} -``` - ---- - -### 7. Geometry -Border radius and shape properties. - -**Structure**: -```typescript -geometry: { - radius: CSSLength; // Standard border radius -} -``` - -**Current Values** (theme-agnostic): -```typescript -geometry: { - radius: "0.5rem", // 8px at default font size -} -``` - -**Usage**: -```css -.button { - border-radius: var(--radius); -} - -/* Derived values */ -.card { - border-radius: calc(var(--radius) - 2px); /* Slightly smaller */ -} -``` - ---- - -### 8. Charts (Optional) -Data visualization color palette. - -**Structure**: -```typescript -charts: { - 1: HSL; - 2: HSL; - 3: HSL; - 4: HSL; - 5: HSL; -} -``` - -**Current Values**: -```typescript -// Dark theme -charts: { - 1: "220 70% 50%", // Blue - 2: "160 60% 45%", // Green - 3: "30 80% 55%", // Orange - 4: "280 65% 60%", // Purple - 5: "340 75% 55%", // Pink -} - -// Light theme (adjusted) -charts: { - 1: "220 70% 45%", - 2: "160 60% 40%", - 3: "30 80% 50%", - 4: "280 65% 50%", - 5: "340 75% 50%", -} -``` - ---- - -## TypeScript Schema - -Complete type definitions for theme system: - -```typescript -// src/types/theme.ts - -/** - * HSL color format: "hue saturation% lightness%" - * Example: "270 100% 70%" - * Note: No hsl() wrapper - used as: hsl(var(--token)) - */ -export type HSL = string; - -/** - * Opacity value between 0 and 1 - */ -export type Opacity = number; - -/** - * CSS length value (px, rem, em, etc.) - */ -export type CSSLength = string; - -/** - * Complete Grimoire theme definition - */ -export interface GrimoireTheme { - /** Unique identifier */ - id: string; - - /** Display name */ - name: string; - - /** Base type for contrast calculations */ - type: "light" | "dark"; - - /** Optional metadata */ - author?: string; - description?: string; - version?: string; - - /** Color tokens */ - colors: { - background: HSL; - foreground: HSL; - card: HSL; - "card-foreground": HSL; - popover: HSL; - "popover-foreground": HSL; - primary: HSL; - "primary-foreground": HSL; - secondary: HSL; - "secondary-foreground": HSL; - muted: HSL; - "muted-foreground": HSL; - accent: HSL; - "accent-foreground": HSL; - destructive: HSL; - "destructive-foreground": HSL; - border: HSL; - input: HSL; - ring: HSL; - }; - - /** Gradient definitions */ - gradients: { - brand: [HSL, HSL, HSL, HSL]; - }; - - /** Syntax highlighting */ - syntax: { - comment: HSL; - keyword: HSL; - string: HSL; - function: HSL; - number: HSL; - operator: HSL; - "operator-opacity"?: Opacity; - deleted: HSL; - "deleted-bg": HSL; - "deleted-bg-opacity": Opacity; - inserted: HSL; - "inserted-bg": HSL; - "inserted-bg-opacity": Opacity; - coord: HSL; - "coord-bg": HSL; - "coord-bg-opacity": Opacity; - }; - - /** Visual effects */ - effects: { - "scrollbar-opacity": Opacity; - "scrollbar-hover-opacity": Opacity; - "overlay-opacity": Opacity; - "preview-opacity": Opacity; - "shadow-color": HSL; - "shadow-blur": CSSLength; - }; - - /** Spacing & sizing */ - spacing: { - "toolbar-height": CSSLength; - "split-width": CSSLength; - "scrollbar-width": CSSLength; - }; - - /** Typography */ - typography: { - "font-family-mono": string; - "font-size-base": CSSLength; - "font-size-code": CSSLength; - "line-height-base": string; - "line-height-code": string; - }; - - /** Geometry */ - geometry: { - radius: CSSLength; - }; -} - -/** - * Chart colors (optional, separate for flexibility) - */ -export interface ChartColors { - 1: HSL; - 2: HSL; - 3: HSL; - 4: HSL; - 5: HSL; -} - -/** - * Complete theme with chart colors - */ -export interface GrimoireThemeComplete extends GrimoireTheme { - charts: ChartColors; -} -``` - ---- - -## Usage Guidelines - -### HSL Format Requirements - -**Correct Format**: -```typescript -"270 100% 70%" // Basic color -"270 100% 70% / 0.5" // With inline opacity -``` - -**❌ Wrong**: -```typescript -"hsl(270 100% 70%)" // Don't wrap in hsl() -"270, 100%, 70%" // Don't use commas -"#B388FF" // Don't use hex -"rgb(179, 136, 255)" // Don't use RGB -``` - -**Rationale**: CSS variables need unwrapped values for flexible composition: -```css -/* ✅ This works */ -color: hsl(var(--accent)); -background: hsl(var(--accent) / 0.1); - -/* ❌ This doesn't */ -color: var(--accent); /* Missing hsl() */ -``` - -### Token Naming Conventions - -1. **Semantic over Visual**: - - ✅ `accent` (semantic: highlight/emphasis) - - ❌ `purple` (visual: describes appearance) - -2. **Purpose over Position**: - - ✅ `card-foreground` (purpose: text on cards) - - ❌ `text-2` (position: arbitrary numbering) - -3. **Component-Specific when Needed**: - - ✅ `toolbar-height` (specific to toolbars) - - ❌ `height-1` (generic, unclear usage) - -### CSS Variable Usage - -**Colors**: -```css -/* ✅ Correct */ -color: hsl(var(--foreground)); -background: hsl(var(--background)); -border: 1px solid hsl(var(--border)); - -/* With opacity */ -background: hsl(var(--accent) / 0.1); -color: hsl(var(--foreground) / var(--scrollbar-opacity)); - -/* ❌ Wrong */ -color: var(--foreground); /* Missing hsl() */ -background: hsl(var(--background), 0.9); /* Wrong opacity syntax */ -``` - -**Gradients**: -```css -/* ✅ Correct */ -background: linear-gradient( - to bottom, - hsl(var(--gradient-brand-1)), - hsl(var(--gradient-brand-2)), - hsl(var(--gradient-brand-3)), - hsl(var(--gradient-brand-4)) -); - -/* ❌ Wrong */ -background: var(--gradient-brand); /* No single gradient variable exists */ -``` - -**Spacing & Sizing**: -```css -/* ✅ Correct - direct usage */ -height: var(--toolbar-height); -width: var(--split-width); - -/* ✅ Correct - with calc */ -padding: calc(var(--toolbar-height) / 2); -``` - -### Contrast Requirements - -**WCAG AA Standards** (minimum): -- **Normal text** (<18pt): 4.5:1 contrast ratio -- **Large text** (≥18pt or bold ≥14pt): 3:1 contrast ratio -- **UI components**: 3:1 contrast ratio - -**Testing**: -```typescript -import { calculateContrast } from "@/lib/theme-utils"; - -// Check contrast -const ratio = calculateContrast( - theme.colors.foreground, - theme.colors.background -); - -if (ratio < 4.5) { - console.warn("Insufficient contrast for normal text"); -} -``` - -**Common Fixes**: -- Light themes: Darken accent colors (50% instead of 70% lightness) -- Dark themes: Lighten accent colors (70% instead of 50% lightness) -- Always test with actual text, not just theory - ---- - -## Theme Creation Workflow - -### 1. Start from Existing Theme - -```typescript -import { GRIMOIRE_DARK } from "@/lib/themes/grimoire-dark"; - -const MY_THEME: GrimoireThemeComplete = { - ...GRIMOIRE_DARK, // Start with dark theme - id: "my-custom-theme", - name: "My Custom Theme", - // Override specific tokens... -}; -``` - -### 2. Modify Colors - -Focus on semantic tokens first: -```typescript -colors: { - ...GRIMOIRE_DARK.colors, - background: "210 30% 10%", // Slightly different blue - accent: "150 80% 60%", // Green accent instead of purple -} -``` - -### 3. Adjust Syntax Highlighting - -Match your color scheme: -```typescript -syntax: { - ...GRIMOIRE_DARK.syntax, - keyword: "150 80% 60%", // Green keywords to match accent - deleted: "0 80% 70%", // Brighter red -} -``` - -### 4. Update Gradients - -Keep brand consistency or create new: -```typescript -gradients: { - brand: [ - "150 80% 60%", // Green - "180 70% 50%", // Cyan - "210 60% 50%", // Blue - "240 70% 60%", // Purple - ], -} -``` - -### 5. Validate Theme - -```typescript -import { validateTheme } from "@/lib/theme-utils"; - -const validation = validateTheme(MY_THEME); - -if (!validation.valid) { - console.error("Theme validation failed:", validation.errors); -} - -if (validation.warnings.length > 0) { - console.warn("Theme warnings:", validation.warnings); -} -``` - -### 6. Test Visually - -- Load theme in browser -- Check all UI components -- Test with real content -- Verify contrast with browser DevTools -- Test in different screen sizes - -### 7. Export & Share - -```typescript -import { exportTheme } from "@/lib/theme-utils"; - -const json = exportTheme(MY_THEME); -// Save to file or share with others -``` - ---- - -## File Structure - -``` -src/ -├── types/ -│ └── theme.ts TypeScript interfaces -│ -├── lib/ -│ ├── themes/ -│ │ ├── index.ts Theme registry & utilities -│ │ ├── grimoire-dark.ts Dark theme definition (const) -│ │ └── grimoire-light.ts Light theme definition (const) -│ │ -│ └── theme-utils.ts Utilities (validate, convert, etc.) -│ -├── index.css CSS variables (all tokens) -└── styles/ - └── prism-theme.css Syntax highlighting styles -``` - ---- - -## Implementation Phases - -### Phase 1: Type System ✅ -- [x] Create `src/types/theme.ts` -- [x] Define all interfaces -- [x] Document token types - -### Phase 2: Theme Definitions -- [ ] Create `src/lib/themes/grimoire-dark.ts` -- [ ] Create `src/lib/themes/grimoire-light.ts` -- [ ] Create `src/lib/themes/index.ts` (registry) -- [ ] Add complete token values for both themes - -### Phase 3: CSS Migration -- [ ] Add all tokens to `:root` in `src/index.css` -- [ ] Add all tokens to `.dark` in `src/index.css` -- [ ] Replace hardcoded colors with token references -- [ ] Update `src/styles/prism-theme.css` to use syntax tokens -- [ ] Update gradient usage in components - -### Phase 4: Utilities -- [ ] Implement `themeToCSSVariables()` in `src/lib/theme-utils.ts` -- [ ] Implement `applyTheme()` for dynamic theme switching -- [ ] Implement `validateTheme()` with contrast checking -- [ ] Implement `exportTheme()` / `importTheme()` for sharing -- [ ] Implement `calculateContrast()` (WCAG compliance) - -### Phase 5: Testing -- [ ] Unit tests for validation functions -- [ ] Unit tests for conversion utilities -- [ ] Visual testing in both themes -- [ ] Contrast testing with automated tools -- [ ] Cross-browser compatibility testing - ---- - -## Benefits of This System - -1. **Complete Abstraction**: All visual properties tokenized, no hardcoded values -2. **Type Safety**: TypeScript enforces token structure and catches errors -3. **Validation**: Automated checks for completeness and contrast -4. **Extensibility**: Easy to add new themes (just define tokens) -5. **Portability**: Import/export themes as JSON -6. **Maintainability**: Single source of truth for visual properties -7. **Consistency**: Enforced visual coherence across all components -8. **Accessibility**: Built-in contrast validation (WCAG compliance) -9. **Future-Proof**: Architecture supports user-defined themes -10. **Developer Experience**: IntelliSense, autocomplete, documentation built-in - ---- - -## Next Steps - -1. **Review this design** - Confirm token structure meets requirements -2. **Approve naming conventions** - Ensure token names are clear and semantic -3. **Begin Phase 2** - Create TypeScript theme definition files -4. **Validate with real data** - Test token system with actual component usage -5. **Iterate** - Refine token structure based on implementation feedback - ---- - -## References - -- **WCAG 2.1 Guidelines**: https://www.w3.org/WAI/WCAG21/Understanding/ -- **Design Tokens W3C Community**: https://design-tokens.github.io/community-group/ -- **HSL Color Model**: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl -- **CSS Custom Properties**: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties -- **Tailwind Color System**: https://tailwindcss.com/docs/customizing-colors -- **Radix Themes**: https://www.radix-ui.com/themes/docs/theme/overview - ---- - -**Document Version**: 1.0 -**Created**: 2025-12-15 -**Author**: Claude (ultrathink analysis) -**Status**: Design Complete - Ready for Implementation diff --git a/claudedocs/event-rendering-system-analysis.md b/claudedocs/event-rendering-system-analysis.md deleted file mode 100644 index 8f8b92a..0000000 --- a/claudedocs/event-rendering-system-analysis.md +++ /dev/null @@ -1,1127 +0,0 @@ -# Event Rendering System - Comprehensive Analysis & Improvement Plan - -**Date**: 2025-12-11 -**Context**: Grimoire Nostr Protocol Explorer -**Scope**: Deep architectural analysis of event rendering system covering 150+ registered event kinds with ~20 custom renderers - ---- - -## Executive Summary - -The current event rendering system has a **solid foundation** with good architectural patterns (registry-based routing, component reuse, type safety), but suffers from **inconsistencies in application** and **missing abstractions** that limit scalability, maintainability, and extensibility. - -**Key Findings:** -- ✅ **Strengths**: Registry pattern, BaseEventContainer, applesauce integration, type safety -- ❌ **Critical Issues**: Hardcoded detail renderers, inconsistent depth tracking, no error boundaries, missing threading abstraction -- 🎯 **Opportunity**: Transform from "working prototype" to "production-grade framework" with systematic improvements - ---- - -## Part 1: Current State Analysis - -### Architecture Overview - -``` -Current System Layers: -┌─────────────────────────────────────┐ -│ Renderer Layer │ Kind1Renderer, Kind6Renderer, etc. -│ (~20 custom, 130+ using default) │ -├─────────────────────────────────────┤ -│ Component Layer │ BaseEventContainer, EventAuthor, EventMenu -│ (Reusable UI components) │ RichText, EmbeddedEvent, MediaEmbed -├─────────────────────────────────────┤ -│ Registry Layer │ kindRenderers map, KindRenderer router -│ (Routing & fallback) │ DefaultKindRenderer -├─────────────────────────────────────┤ -│ Data Layer │ EventStore (applesauce), useNostrEvent hook -│ (Reactive state) │ RelayPool, Dexie cache -└─────────────────────────────────────┘ -``` - -### Event Kind Categories - -Analysis of 150+ registered kinds reveals **7 fundamental patterns**: - -1. **Content-Primary** (1, 30023, 9802) - - Main payload in `content` field - - Rich text rendering, markdown, media embeds - - Examples: Notes, articles, highlights - -2. **Reference Events** (6, 7, 9735) - - Point to other events via e/a tags - - Embed referenced content - - Examples: Reposts, reactions, zaps - -3. **Metadata Events** (0, 3, 10002) - - Structured data in content JSON - - Key-value pairs, lists, configurations - - Examples: Profiles, contacts, relay lists - -4. **List Events** (30000-39999 replaceable) - - Arrays of items in tags - - Follow sets, mute lists, bookmarks - - Addressable/replaceable nature - -5. **Media Events** (20, 21, 22, 1063) - - Content is URLs with metadata - - Images, videos, files - - Thumbnails, dimensions, MIME types - -6. **Action Events** (5, 1984) - - Represent operations on other events - - Deletions, reports, moderation - - Usually invisible to end users - -7. **Communication Events** (4, 14, 1111) - - Threaded messaging - - DMs, comments, chat messages - - Multiple threading models (NIP-10, NIP-22, NIP-28) - ---- - -## Part 2: What's Common to All Events - -### Universal Requirements - -Every event, regardless of kind, needs: - -1. **Author Context** - WHO created this - - Profile info (name, avatar, NIP-05) - - Clickable to open profile - - Badge/verification indicators - -2. **Temporal Context** - WHEN was this created - - Relative timestamps ("2h ago") - - Absolute time on hover (ISO format) - - Locale-aware formatting - -3. **Event Identity** - WHAT is this - - Kind badge with icon and name - - Event ID (bech32 format: nevent/naddr) - - Copy/share capabilities - -4. **Actions** - User operations - - Open in detail view - - Copy event ID - - View raw JSON - - (Future: Reply, React, Zap, Share) - -5. **Relay Context** - WHERE was this seen - - List of relays that served the event - - Relay health indicators - - Relay preferences for publishing - -6. **Addressability** - HOW to reference - - Regular events: nevent (id + relays + author) - - Addressable events: naddr (kind + pubkey + identifier + relays) - - note1 (deprecated but still supported) - -### Current Implementation - -**Well-Handled:** -- ✅ Author, Temporal, Identity, Actions (1-4) via `BaseEventContainer` + `EventMenu` -- ✅ Addressability (6) logic in EventDetailViewer - -**Missing Universally:** -- ❌ Signature verification indicator -- ❌ Edit history (NIP-09 deletion event tracking) -- ❌ Engagement preview (reply count, zap total, reaction summary) -- ❌ Related events indicator -- ❌ Community/context badges (NIP-72 communities, NIP-29 groups) - -**Recommendation:** Extend `BaseEventContainer` with optional engagement footer and verification indicator. - ---- - -## Part 3: Rendering Context Analysis - -### Three Primary Contexts - -1. **Feed/Timeline** - Compact, scannable view - - Emphasis on density, quick scanning - - Show summary/preview, not full content - - Inline media thumbnails - - Minimal interaction chrome - -2. **Detail** - Expansive, full-content view - - Emphasis on readability, completeness - - Full markdown rendering, full-size media - - Show relationships (replies, zaps, reactions) - - Additional metadata and actions - -3. **Embedded** - Nested preview within another event - - Context-aware depth limiting - - Minimal chrome (no duplicate headers if already in context) - - Click to expand/navigate - - Performance-conscious (lazy load) - -### Current Implementation - -**Feed Rendering:** -- ✅ Works well with `KindRenderer` + `BaseEventContainer` -- ✅ Consistent pattern across all kinds -- ⚠️ No virtualization for performance - -**Detail Rendering:** -- ❌ **CRITICAL**: Hardcoded switch statement in `EventDetailViewer.tsx`: - ```tsx - event.kind === kinds.Metadata ? - : event.kind === kinds.Contacts ? - : event.kind === kinds.LongFormArticle ? - : - ``` -- ❌ Breaks registry pattern - not extensible -- ❌ Only 5 kinds have detail renderers, rest fallback to feed - -**Embedded Rendering:** -- ⚠️ Uses same as feed (via `EmbeddedEvent` → `KindRenderer`) -- ⚠️ No context awareness -- ⚠️ Depth tracking inconsistent - -### Recommended Architecture - -**Unified Registry Pattern:** -```tsx -// Proposed structure -export const kindRenderers: KindRendererRegistry = { - 1: { - feed: Kind1Renderer, - detail: Kind1DetailRenderer, // optional, fallback to feed - embed: Kind1EmbedRenderer, // optional, fallback to feed - }, - // Or simplified: - 1: Kind1Renderer, // if no variants needed - 30023: { - feed: Kind30023Renderer, // Compact: title + summary - detail: Kind30023DetailRenderer, // Full markdown + relationships - } -}; - -// Usage -function KindRenderer({ event, context = 'feed' }) { - const registry = kindRenderers[event.kind]; - const Renderer = registry?.[context] || registry?.feed || registry || DefaultKindRenderer; - return ; -} -``` - -**Benefits:** -- ✅ Consistent pattern for all contexts -- ✅ Extensible - add detail renderers without modifying router -- ✅ Self-documenting - registry shows available variants -- ✅ Type-safe - validate registry at compile time - ---- - -## Part 4: Depth Tracking & Nesting - -### The Problem - -Events can reference other events infinitely: -- Kind 6 (repost) of Kind 6 of Kind 6... → infinite loop -- Kind 1 (note) replying to Kind 1 replying to Kind 1... → deep nesting -- Kind 9735 (zap) of article containing zaps... → exponential expansion - -### Current State - -- ✅ `Kind1Renderer` passes `depth` to `RichText` -- ✅ `RichText` uses depth to limit nesting -- ❌ `Kind6Renderer` (repost) doesn't track depth → infinite loop possible -- ❌ `Kind9735Renderer` (zap) embeds without depth → can nest infinitely -- ❌ `EmbeddedEvent` doesn't auto-increment depth - -### Solution: Systematic Depth Management - -```tsx -// 1. Universal depth constant -export const MAX_EMBED_DEPTH = 3; - -// 2. All renderers receive and honor depth -export interface BaseEventProps { - event: NostrEvent; - depth?: number; - context?: 'feed' | 'detail' | 'embed'; -} - -// 3. EmbeddedEvent auto-increments -export function EmbeddedEvent({ eventId, depth = 0, ...props }) { - const event = useNostrEvent(eventId); - if (!event) return ; - - if (depth >= MAX_EMBED_DEPTH) { - return ; - } - - return ; -} - -// 4. Depth-aware rendering -export function Kind6Renderer({ event, depth = 0 }) { - if (depth >= MAX_EMBED_DEPTH) { - return -
Repost of
-
; - } - - return -
Reposted
- -
; -} -``` - -**Benefits:** -- ✅ Prevents infinite loops -- ✅ Improves performance (limits cascade fetching) -- ✅ Better UX (collapsed deep threads with expand option) -- ✅ Consistent behavior across all renderers - ---- - -## Part 5: Threading & Reply Abstraction - -### The Challenge - -Multiple threading models exist in Nostr: - -1. **NIP-10** (Kind 1 notes) - - `e` tags with markers: `["e", id, relay, "root"|"reply"]` - - Root = original post, Reply = immediate parent - - Mentions = other referenced events - -2. **NIP-22** (Kind 1111 comments) - - **Uppercase tags** = root scope: `K`, `E`, `A`, `I`, `P` - - **Lowercase tags** = parent item: `k`, `e`, `a`, `i`, `p` - - Can thread on events OR external identifiers (URLs, podcasts, etc.) - - MUST NOT reply to kind 1 notes (use kind 1 instead) - -3. **NIP-28** (Kind 42 channel messages) - - Replies within channel context - - Different tag structure - -4. **NIP-29** (Kinds 10, 11, 12 group messages) - - Group-specific threading - - Additional permissions layer - -### Current Implementation - -- ✅ Kind1Renderer shows NIP-10 reply indicator -- ✅ Uses `getNip10References` from applesauce -- ❌ No support for NIP-22 (Kind 1111 not implemented) -- ❌ No support for other threading models -- ❌ No generic threading components - -### Proposed Abstraction - -**Helper Layer:** -```tsx -// src/lib/threading.ts -export interface ThreadReference { - type: 'nip10' | 'nip22' | 'nip28' | 'nip29'; - root?: EventPointer | AddressPointer | string; // string for external (NIP-22) - parent?: EventPointer | AddressPointer | string; - mentions?: Array; - rootAuthor?: string; - parentAuthor?: string; -} - -export function getThreadReferences(event: NostrEvent): ThreadReference | null { - // Detect threading model by kind and tags - if (event.kind === 1) return getNip10Thread(event); - if (event.kind === 1111) return getNip22Thread(event); - if (event.kind === 42) return getNip28Thread(event); - // ... etc - return null; -} -``` - -**Component Layer:** -```tsx -// Generic thread indicator -export function ThreadIndicator({ event, depth = 0 }) { - const refs = getThreadReferences(event); - if (!refs) return null; - - const parentEvent = useNostrEvent(refs.parent); - - return ( -
- - Replying to - {parentEvent ? ( - - ) : refs.type === 'nip22' && typeof refs.parent === 'string' ? ( - - ) : ( - - )} -
- ); -} - -// Generic thread tree (for detail view) -export function ThreadTree({ rootEvent }) { - const replies = useReplies(rootEvent); - return ( -
- {replies.map(reply => ( - - ))} -
- ); -} -``` - -**Benefits:** -- ✅ Single component works across all threading models -- ✅ Extensible to new NIPs -- ✅ Reusable across different renderers -- ✅ Consistent UX for users -- ✅ Easier to maintain (one place to fix threading bugs) - ---- - -## Part 6: Metadata Extraction Patterns - -### Current Approaches - -1. **Applesauce Helpers** ✅ (GOOD) - - `getArticleTitle`, `getZapAmount`, `getNip10References` - - Well-tested, consistent, handles edge cases - - Examples: `Kind30023Renderer`, `Kind9735Renderer` - -2. **Manual Tag Parsing** ⚠️ (INCONSISTENT) - - `event.tags.find(t => t[0] === "e")` - - Error-prone, repeated code, misses edge cases - - Examples: `Kind6Renderer`, various places - -3. **JSON Parsing** ❌ (ERROR-PRONE) - - `JSON.parse(event.content)` without try/catch - - Can crash entire app if malformed - - Examples: Profile metadata, relay lists - -### What Applesauce Provides - -Currently has helpers for: -- ✅ Articles (30023): title, summary, published, image -- ✅ Zaps (9735): amount, sender, request, pointers -- ✅ Threading (1): NIP-10 references -- ✅ Profiles (0): metadata parsing -- ✅ Relays: seen relays, relay hints - -### What's Missing - -Need helpers for: -- ❌ File metadata (1063): url, hash, size, mime, dimensions -- ❌ Media events (20, 21, 22): URLs, thumbnails, dimensions -- ❌ List events (30000+): systematic list item extraction -- ❌ Comments (1111): NIP-22 uppercase/lowercase tag parsing -- ❌ Reactions (7): emoji normalization (+ → ❤️) -- ❌ Reposts (6, 16, 18): reposted event extraction -- ❌ Highlights (9802): context, highlight text -- ❌ Calendar events (31922-31925): date/time parsing -- ❌ Polls (1068): options, votes, tally -- ❌ Communities (34550): community info extraction - -### Recommendation - -**Architecture Principle:** Renderers should NEVER parse tags/content directly. - -```tsx -// BAD ❌ -const eTag = event.tags.find(t => t[0] === "e")?.[1]; - -// GOOD ✅ -import { getRepostedEvent } from '@/lib/helpers/repost'; -const repostPointer = getRepostedEvent(event); -``` - -**Action Items:** -1. Contribute missing helpers to applesauce-core (if generic) -2. Create local helper library for Grimoire-specific needs -3. Audit all renderers, replace manual parsing with helpers -4. Enforce via ESLint rule: no direct `event.tags.find` - ---- - -## Part 7: Performance & Scalability - -### Current Bottlenecks - -1. **No Virtualization** - - All events in feed render immediately - - 1000 events = 1000 DOM nodes = slow scroll - - Wastes memory on off-screen content - -2. **No Memoization** - - RichText parses content on every render - - Profile lookups happen repeatedly - - JSON.parse re-runs unnecessarily - - Expensive computations not cached - -3. **No Lazy Loading** - - All renderer code loaded upfront - - ~20 renderer components = large initial bundle - - Could code-split by kind - -4. **Heavy Base Components** - - Every event has `BaseEventContainer` overhead - - Profile fetch for every `EventAuthor` - - Could batch profile fetches - -5. **Cascading Fetches** - - Embedded event triggers fetch - - That event might embed another - - Exponential growth without depth limiting - -### Good News - -- ✅ EventStore handles deduplication -- ✅ Dexie provides offline caching -- ✅ Reactive system (RxJS) is efficient - -### Solutions - -**1. Virtual Scrolling** -```tsx -import { Virtuoso } from 'react-virtuoso'; - -function EventFeed({ events }) { - return ( - ( - - )} - /> - ); -} -``` - -**2. Memoization** -```tsx -// Wrap all renderers -export const Kind1Renderer = React.memo(({ event, depth }) => { - const refs = useMemo(() => getNip10References(event), [event.id]); - const handleClick = useCallback(() => {...}, [event.id]); - - return - - ; -}); -``` - -**3. Code Splitting** -```tsx -// Lazy load detail renderers -const Kind30023DetailRenderer = lazy(() => - import('./kinds/Kind30023DetailRenderer') -); - -// Use with Suspense -}> - - -``` - -**4. Batch Profile Fetches** -```tsx -// Instead of individual useProfile in every EventAuthor -// Batch load all visible profiles -function EventFeed({ events }) { - const pubkeys = useMemo(() => - events.map(e => e.pubkey), [events] - ); - useBatchProfiles(pubkeys); // Prefetch - - return events.map(e => ); -} -``` - -**Performance Targets:** -- Feed with 10,000 events: Smooth 60fps scroll -- Initial render: < 100ms -- Event interaction: < 50ms response -- Bundle size: < 300KB for core, lazy load rest - ---- - -## Part 8: Error Handling & Resilience - -### Current Error Scenarios - -1. **Malformed Events** - - Invalid JSON in content - - Missing required tags - - Incorrect tag structure - -2. **Network Failures** - - Relays timeout - - Event not found - - Incomplete data - -3. **Parsing Failures** - - Markdown rendering errors - - NIP-19 decode failures - - Media load failures - -4. **Rendering Errors** - - Component crashes - - Infinite loops (depth issue) - - Out of memory - -### Current Handling - -- ⚠️ Some try/catch in parsers (inconsistent) -- ⚠️ EmbeddedEvent shows "Loading..." forever if fetch fails -- ❌ No error boundaries around renderers -- ✅ DefaultKindRenderer for unknown kinds (good!) - -### Solution: Error Boundaries - -```tsx -// Per-event error boundary -export function EventErrorBoundary({ children, event }) { - return ( - ( -
- -
-

Failed to render event

-

- Kind {event.kind} • {event.id.slice(0, 8)} -

-
- Error details -
{error.message}
-
-
-
- - - -
-
- )} - > - {children} -
- ); -} - -// Usage in feed -{events.map(event => ( - - - -))} -``` - -**Benefits:** -- ✅ One broken event doesn't break entire feed -- ✅ User gets actionable error info -- ✅ Developer gets diagnostics -- ✅ Graceful degradation - ---- - -## Part 9: Accessibility & Internationalization - -### Accessibility Gaps - -**Keyboard Navigation:** -- ❌ No keyboard shortcuts for common actions -- ❌ Can't navigate between events with Tab -- ❌ Can't expand/collapse without mouse - -**Screen Reader Support:** -- ❌ EventMenu has no aria-label -- ❌ Embedded events don't announce properly -- ❌ Time stamps are "2h ago" but no absolute time for SR -- ⚠️ BaseEventContainer uses `
` not `
` - -**Visual:** -- ⚠️ Muted text might not meet WCAG AA contrast -- ❌ No prefers-reduced-motion support -- ❌ Focus indicators inconsistent - -**RTL Support:** -- ❌ Noted in TODO as partially implemented -- ❌ Inline elements conflict with RTL alignment - -### I18n Gaps - -- ⚠️ Timestamps use locale from state but inconsistently -- ❌ Number formatting hardcoded to "en" (zap amounts) -- ❌ Kind names are English-only strings -- ❌ Error messages hardcoded English -- ❌ No language detection for content - -### Solutions - -**Semantic HTML:** -```tsx -export function BaseEventContainer({ event, children }) { - return ( -
-
- - - -
-
{children}
-
- ); -} -``` - -**Keyboard Navigation:** -```tsx -// Arrow keys to navigate events -// Enter to open detail -// Escape to close -// Tab to focus actions -useKeyboardNavigation({ - onUp: () => focusPrevEvent(), - onDown: () => focusNextEvent(), - onEnter: () => openEventDetail(), -}); -``` - -**I18n:** -```tsx -import { useTranslation } from 'react-i18next'; - -export function EventMenu({ event }) { - const { t } = useTranslation(); - return ( - - {t('event.actions.copy_id')} - - ); -} -``` - ---- - -## Part 10: Developer Experience - -### Current DX - -**Good:** -- ✅ Clear file structure (`src/components/nostr/kinds/`) -- ✅ TypeScript types (`BaseEventProps`) -- ✅ README documenting pattern -- ✅ Consistent naming (`KindXRenderer`) - -**Friction:** -- ❌ Can't hot-reload new renderer without modifying `index.tsx` -- ❌ No component gallery (Storybook) -- ❌ Hard to test renderers in isolation -- ❌ Manual registration in multiple places -- ❌ No development tooling (event inspector) -- ❌ No renderer generator CLI - -### Ideal DX - -**1. Convention-Based Registration** -```bash -# Just create the file, auto-discovered -src/components/nostr/kinds/Kind1111Renderer.tsx -# No need to modify index.tsx -``` - -**2. Component Gallery** -```tsx -// Visit /dev/renderers in dev mode -// Browse all renderers with sample events -// Test with different contexts (feed/detail/embed) -// Inspect props, performance -``` - -**3. Testing Utilities** -```tsx -import { renderKind, mockEvent } from '@/test/utils'; - -test('Kind1Renderer displays content', () => { - const event = mockEvent({ kind: 1, content: 'Hello' }); - const { getByText } = renderKind(1, event); - expect(getByText('Hello')).toBeInTheDocument(); -}); -``` - -**4. Generator CLI** -```bash -npm run generate:renderer -- --kind 1111 --nip 22 -# Scaffolds: -# - Kind1111Renderer.tsx with boilerplate -# - Kind1111Renderer.test.tsx -# - Updates registry -# - Adds to documentation -``` - -**5. Dev Tools** -```tsx -// Browser extension or dev panel - - - -// Shows: props, state, performance, helper calls, errors -``` - ---- - -## Part 11: What's Working Well (To Preserve) - -These patterns are **strengths** to maintain and enhance: - -1. ✅ **Registry Pattern**: Centralized kind → renderer mapping -2. ✅ **BaseEventContainer**: Consistent header/footer -3. ✅ **Applesauce Integration**: Using library helpers -4. ✅ **Type Safety**: TypeScript interfaces -5. ✅ **Separation of Concerns**: Rendering separate from data fetching -6. ✅ **Recursive Rendering**: KindRenderer can nest -7. ✅ **Universal Actions**: EventMenu available everywhere -8. ✅ **Event Identity**: Good handling of regular vs addressable -9. ✅ **Default Fallback**: Unknown kinds still display -10. ✅ **Component Reuse**: EmbeddedEvent, MediaEmbed, RichText - -**Don't throw away these foundations - build on them!** - ---- - -## Part 12: Comprehensive Improvement Roadmap - -### Phase 1: Foundation Fixes (1-2 weeks) -**Goal:** Fix critical architectural issues and quick wins - -**1.1 Unified Detail Renderer Registry** -- Remove hardcoded switch in EventDetailViewer -- Create `detailRenderers` map parallel to `kindRenderers` -- Fallback logic: detail → feed → default -- Files: `src/components/nostr/kinds/index.tsx`, `EventDetailViewer.tsx` -- **Impact:** HIGH | **Effort:** LOW - -**1.2 Systematic Depth Tracking** -- Add `MAX_EMBED_DEPTH` constant -- Update `BaseEventProps` to require depth -- Audit all renderers using `EmbeddedEvent` -- Implement `CollapsedPreview` for max depth -- Files: All `*Renderer.tsx` files -- **Impact:** HIGH | **Effort:** MEDIUM - -**1.3 Error Boundaries** -- Create `EventErrorBoundary` component -- Wrap all events in feeds -- Add diagnostic error cards -- File: `src/components/EventErrorBoundary.tsx` -- **Impact:** HIGH | **Effort:** LOW - -**1.4 Fix JSON Viewer Scrolling** -- From TODO: "JSON viewer scrolling" -- Add `overflow-auto` and `max-height` to JSON container -- File: `src/components/JsonViewer.tsx` -- **Impact:** MEDIUM | **Effort:** TRIVIAL - -**1.5 Renderer Memoization** -- Wrap all renderer components with `React.memo` -- Add `useMemo` for expensive computations -- Add `useCallback` for handlers -- Files: All `*Renderer.tsx` files -- **Impact:** MEDIUM | **Effort:** LOW - -**Deliverables:** -- [ ] Detail renderer registry implemented -- [ ] All renderers honor depth (with tests) -- [ ] Error boundaries deployed -- [ ] JSON viewer scrolls properly -- [ ] All renderers memoized - ---- - -### Phase 2: Component Library (2-3 weeks) -**Goal:** Build reusable abstractions for common patterns - -**2.1 Generic Threading Components** -- `getThreadReferences()` helper supporting NIP-10, NIP-22, NIP-28 -- `` component -- `` for parent preview -- `` for detail view reply chains -- Files: `src/lib/threading.ts`, `src/components/Thread/` -- **Impact:** HIGH | **Effort:** HIGH - -**2.2 NIP-22 Comment Support** -- Implement `Kind1111Renderer` (from TODO) -- NIP-22 tag parsing helpers (K/k, E/e, A/a, I/i, P/p) -- External identifier display (I tags) -- Nested comment threading -- Files: `src/lib/helpers/nip22.ts`, `src/components/nostr/kinds/Kind1111Renderer.tsx` -- **Impact:** HIGH | **Effort:** HIGH - -**2.3 Relationship Panels** -- `` - Show replies to event -- `` - Show zaps with total/list -- `` - Group reactions by emoji -- `` - Universal engagement indicators -- Use in detail renderers -- Files: `src/components/nostr/Relationships/` -- **Impact:** MEDIUM | **Effort:** MEDIUM - -**2.4 Enhanced Media Components** -- Multi-stage rendering (placeholder → thumbnail → full → error) -- Lazy loading with IntersectionObserver -- NSFW blur with content-warning tag support -- Quality selection for videos -- Accessibility improvements (alt text, captions) -- Files: Enhance `src/components/nostr/MediaEmbed.tsx` -- **Impact:** MEDIUM | **Effort:** MEDIUM - -**2.5 Context-Aware Rendering** -- Add `context` prop to BaseEventProps -- Renderers adapt to feed vs detail vs embed -- Update all existing renderers -- Files: `src/components/nostr/kinds/index.tsx`, all renderers -- **Impact:** MEDIUM | **Effort:** LOW - -**Deliverables:** -- [ ] Threading works across NIP-10, NIP-22, NIP-28 -- [ ] Kind 1111 (comments) fully functional -- [ ] Detail views show relationships -- [ ] Media rendering has all stages -- [ ] Context awareness implemented - ---- - -### Phase 3: Architecture Evolution (3-4 weeks) -**Goal:** Transform into production-grade framework - -**3.1 Performance Optimization** -- Virtual scrolling with react-virtuoso -- Code splitting for detail renderers -- Batch profile fetching -- Suspense boundaries -- Performance monitoring -- Files: `src/components/ReqViewer.tsx`, `EventDetailViewer.tsx` -- **Impact:** HIGH | **Effort:** MEDIUM - -**3.2 Helper Library Expansion** -- Audit all renderers for manual tag parsing -- Create helpers for all missing NIPs: - - File metadata (1063) - - Media events (20, 21, 22) - - Lists (30000+) - - Reposts (6, 16, 18) - - Highlights (9802) - - Calendar (31922-31925) - - Polls (1068) -- Submit generic ones to applesauce-core -- Files: `src/lib/helpers/` directory structure -- **Impact:** HIGH | **Effort:** HIGH - -**3.3 Accessibility Improvements** -- Semantic HTML (`
`, `