Skip to content

refactor(editor): replace DOM manipulation with React node views and floating-ui#253

Merged
purrgrammer merged 10 commits intomainfrom
claude/refactor-editor-react-9absy
Mar 3, 2026
Merged

refactor(editor): replace DOM manipulation with React node views and floating-ui#253
purrgrammer merged 10 commits intomainfrom
claude/refactor-editor-react-9absy

Conversation

@purrgrammer
Copy link
Owner

  • Convert all inline node views (emoji, blob attachment, event preview) from
    imperative document.createElement() to React components via ReactNodeViewRenderer
  • Replace tippy.js with @floating-ui/react-dom for suggestion popup positioning
  • Create useSuggestionRenderer hook bridging Tiptap suggestion callbacks to React state
  • Extract shared EmojiMention, SubmitShortcut, and inline node extensions to separate files
  • Extract types (EmojiTag, BlobAttachment, SerializedContent) to editor/types.ts
  • Extract serialization logic to editor/utils/serialize.ts
  • Remove redundant DOM keydown listener from RichEditor (handled by SubmitShortcut extension)
  • Remove tippy.js dependency (-1045 lines net, RichEditor 632→297, MentionEditor 1038→354)

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u

@vercel
Copy link

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
grimoire Ready Ready Preview, Comment Mar 3, 2026 8:49pm

Request Review

claude and others added 10 commits March 3, 2026 21:35
…floating-ui

- Convert all inline node views (emoji, blob attachment, event preview) from
  imperative document.createElement() to React components via ReactNodeViewRenderer
- Replace tippy.js with @floating-ui/react-dom for suggestion popup positioning
- Create useSuggestionRenderer hook bridging Tiptap suggestion callbacks to React state
- Extract shared EmojiMention, SubmitShortcut, and inline node extensions to separate files
- Extract types (EmojiTag, BlobAttachment, SerializedContent) to editor/types.ts
- Extract serialization logic to editor/utils/serialize.ts
- Remove redundant DOM keydown listener from RichEditor (handled by SubmitShortcut extension)
- Remove tippy.js dependency (-1045 lines net, RichEditor 632→297, MentionEditor 1038→354)

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
…le click behavior

- Replace UserName component in ProfileSuggestionList with plain text display
  so clicking a suggestion autocompletes instead of opening their profile
  (UserName has an onClick that calls addWindow and stopPropagation)
- Add react-virtuoso to ProfileSuggestionList for efficient lazy rendering
  of up to 20 search results with fixed item height scrolling
- Add profile avatars with lazy loading and initial-letter fallback
- Fix SuggestionPopover positioning with autoUpdate for scroll/resize tracking
- Add size middleware to constrain popover max-height to available viewport space

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
…st with Virtuoso

Replace the 8-column grid layout with a vertical list matching the profile
suggestion style — each row shows the emoji preview alongside its :shortcode:
name. Uses react-virtuoso with fixedItemHeight for lazy rendering and smooth
keyboard-driven scrolling through large emoji sets.

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
When backspacing over a mention-based node, Tiptap inserts the node's
mentionSuggestionChar attribute as undo text. The EmojiMention extension
inherits Mention's default of '@', so deleting an emoji left '@' instead
of ':'. Fix by explicitly setting mentionSuggestionChar: ':' in the emoji
command's attrs for both RichEditor and MentionEditor.

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
Tests all 8 custom extensions using headless TipTap Editor instances in
jsdom environment (TipTap has no official testing package):

- EmojiMention: schema, renderText (unicode vs custom), mentionSuggestionChar
  attribute handling, backspace behavior regression test
- BlobAttachmentRichNode/InlineNode: schema (block vs inline), attributes,
  renderText URL serialization, parseHTML selectors
- NostrEventPreviewRichNode/InlineNode: schema, renderText encoding for
  note/nevent/naddr back to nostr: URIs
- SubmitShortcut: Mod-Enter always submits, Enter behavior with
  enterSubmits flag
- FilePasteHandler: media type filtering (image/video/audio), non-media
  rejection, mixed paste filtering, edge cases (no files, no callback)
- NostrPasteHandler: bech32 regex matching (npub/note/nevent/naddr/nprofile),
  nostr: prefix handling, URL exclusion, node creation (mention vs preview),
  surrounding text preservation, multiple entities
- Serialization: formatBlobSize, serializeRichContent (emoji tag extraction,
  blob dedup, address refs), serializeInlineContent (mention→nostr: URI,
  emoji→shortcode, blob→URL, event preview encoding)

90 new tests total.

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
…al testing

NostrPasteHandler fixes:
- Punctuation after bech32 now matches (npub1..., npub1...! etc.)
  Changed trailing lookahead from (?=$|\s) to (?=$|\s|[.,!?;:)\]}>])
- Fixed double-space between entities — unconditional " " after every
  entity caused doubled spaces. Now only adds trailing space when entity
  is at the very end of pasted text (for cursor positioning).
- Tightened regex character class from [\w] to [a-z0-9] to match actual
  bech32 charset (rejects uppercase, underscore)
- Wrapped dispatch in try/catch to handle block-node-at-inline-position
  errors gracefully (falls back to default paste)

Serialization fix:
- serializeRichContent now guards blob collection with `url && sha256`
  matching the defensive checks already in serializeInlineContent.
  Previously null sha256 would corrupt the dedup Set and null url would
  produce invalid BlobAttachment entries.

Added 22 new edge case tests:
- Paste handler: punctuation boundaries, double-space regression,
  malformed bech32 fallback, uppercase rejection, error resilience
- Serialization: empty editor, null sha256/url blobs, invalid pubkey
  fallback, missing mention attrs, inline dedup, multi-paragraph

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
Both suggestion dropdowns use Virtuoso for virtualized rendering, so
they can handle large result sets without performance issues. The
previous limits (20 profiles, 24 emojis) were too restrictive — users
with many custom emojis sharing a substring or large contact lists
couldn't scroll to find the right match.

Raised both limits to 200 to allow thorough browsing while still
bounding the result set.

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
Replace the fixed 1-row grid (8 emojis) with a scrollable virtualized
list matching the editor's EmojiSuggestionList look & feel:

- Search box at top with magnifying glass icon
- Virtuoso-backed scrollable list (8 visible items, unlimited results)
- Each row shows emoji icon + :shortcode: label
- Keyboard navigation: arrow keys to select, Enter to confirm
- Mouse hover highlights, click selects
- Frequently used emojis still shown first when no search query
- Narrower dialog (max-w-xs) for a compact picker feel

https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
…ptions

- Add optional `address` field to EmojiTag in editor/types.ts to match
  NIP-30 changes from main (30030 emoji set address)
- Extend GroupMessageOptions with MetaTagOptions to fix type error in
  GroupMessageBlueprint's setMetaTags call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…roll

- Restore `address` attribute in shared EmojiMention extension (emoji.ts)
  that was dropped during refactor — required for NIP-30 emoji set tracking
- Extract `address` from emoji nodes in both serializeRichContent and
  serializeInlineContent so it makes it into published events
- Fix MentionEditorProps.onSubmit signature: use EmojiTag[] (not the narrower
  inline type) so address field flows through to callers
- Restore UserName component in ProfileSuggestionList for proper display
  with Grimoire member badges and supporter flame
- Remove scrollbar when all items fit: set overflow:hidden on Virtuoso when
  items.length <= MAX_VISIBLE (profile list, emoji list, emoji picker dialog)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@purrgrammer purrgrammer force-pushed the claude/refactor-editor-react-9absy branch from 8b74c04 to 5e2e2e9 Compare March 3, 2026 20:49
@purrgrammer purrgrammer merged commit 8c9ecd5 into main Mar 3, 2026
4 checks passed
@purrgrammer purrgrammer deleted the claude/refactor-editor-react-9absy branch March 3, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants