feat(composer): hand-rolled contenteditable editor engine - #33
Conversation
…omes Composer.TextareaLegacy
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…verride semantics
|
Added the native-textarea callback surface: |
…ak dismisses active trigger token
|
Two more additions from testing: 1. Soft break dismisses an active trigger token (the screenshot bug): the tracker's sticky range was absorbing inserted newlines, dragging the badge across lines and keeping the popup open. Now: Shift+Enter while the popup is open dismisses the token Escape-style (with re-entry suppression) and then inserts the break; the tracker additionally enforces that a token can never span a line break (covers pasted newlines too). 2. Checklist additions:
|
|
Fix for the reported disappearing-editor bug ( Checklist addition:
|
|
Fix for the two-line-editor screenshot ( |
|
Heads-up: this PR's squash landed on |
…33) (#37) * feat(composer): hand-rolled contenteditable editor engine; TipTap becomes Composer.TextareaLegacy * feat(composer): editable-routed event callbacks with consumer-first override semantics * fix(composer): hide placeholder during IME composition; add native placeholder prop * refactor(composer): single-writer hasContent; placeholder gates on engine isComposing * feat(composer): submitOn prop for Enter/Shift+Enter mapping; soft break dismisses active trigger token * fix(composer): repaint on br-count drift — collapse on emptied editor + phantom newlines * fix(composer): whitespace-only docs count as content — placeholder no longer overlays line breaks * feat(composer): ghost-text completion of the highlighted command item (#35) * feat(composer): ghost-text completion of the highlighted command item * style(composer): borderless chip + badge surface in the input * feat(composer): per-prefix suggestion flag gating ghost-text completion * feat(composer): per-prefix command placeholder, exclusive with suggestion * refactor(composer): badge hints as a real command-hint slot element, replacing ::after attribute CSS
PR 2 of 3 — the replacement editor engine, stacked on #32.
Composer.Textareais now a hand-rolled contenteditable over a flat{text|chip}[]segment model; the TipTap implementation stays mountable asComposer.TextareaLegacyfor side-by-side comparison until PR 3 deletes it.Design invariants: the browser owns text editing (native typing/IME; model syncs from the DOM via read → diff → commit); programmatic ops are the only imperative DOM writes; engine state lives outside React (version-counter subscription, chip visuals via portals into engine-owned spans).
segments.ts— pure kernel: splice/slice,mapPosition(PM bias semantics),diffFlatText,planBeforeInputallow-list policy, snapshot converters (engine-portable under theComposerSnapshotbrand)trigger-tracker.ts— sticky-token port of the PM plugin (spaces in query,dismissedAtsuppression)editor-dom.ts— DOM read-back (padding-<br>pact, NBSP normalization, badge-span transparency), chip-span reuse keeps React portals stableuse-composer-editor.tsx— engine factory + hook; fullRegisteredEditor; IME composition guard incl. keyCode-229 Enter protection; blocked native undo (parity: the TipTap setup shipped no History extension)maxLength(chip = 1),required,name(hidden-input FormData mirror), forwarded natives (spellCheck,enterKeyHint,inputMode,dir,aria-*); baked-inrole="textbox"/aria-multilineAccepted divergences:
onValueChangeemits"\n"per soft break (legacy:"\n\n"); copy/cut emit chip-markdown; in-editor text drag-drop denied in v1; IMEmaxLengthenforced at composition commit.Verified: 156/156 tests (68 new),
tsc --noEmitclean, biome clean, package builds. Store/command-list/popover/container untouched.Manual checklist (before merge)
Compare against legacy by pointing the styled
ComposerTextareaincomponents/ai/composer.tsxatComposerPrimitive.TextareaLegacy.@opens; badge + "Type to filter" hint; spaces in query keep it open; Escape dismissal suppression; ArrowLeft/Right caret trap; click-outside closes; Tab/Enter select → chip + trailing spacenamemirrors serialized text into FormData;requiredblocks empty submitmax-hcap → scrolling; trailing empty line reachable