feat(dom): lvt-fx:text-select — character-range selection over diff text - #140
Conversation
Adds a native-selection capture directive so a host can let users comment on
a word / phrase / multi-line span (not just whole lines). Mirrors
region-select's arm/sweep/teardown lifecycle.
- textOffsetsFromSelection: resolves a native selection to rune-offset
{fromLine,fromCol,toLine,toCol,side,text} against [data-line]/[data-side]
rows with a [data-line-text] gutter-excluding container; rejects cross-side
and out-of-content boundaries.
- Debounced selectionchange trigger → floating Comment button (below the
selection on coarse pointers so iOS's native callout doesn't cover it).
- Client-only keyboard block caret: visible on load, ←/→ move column, ↑/↓
follow the host's line cursor, Shift+arrows extend via getSelection().modify()
on non-editable text (no contenteditable), Ctrl/Alt = word, Enter commits.
- Capture-phase click guard so a drag-select's click never fires the host's
line action.
Styling is overridable via --lvt-text-select-* / --lvt-text-caret-* custom
properties. Covered by tests/text-select.test.ts (jsdom).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ey41dRwDpRgLtVHZjhYLKt
|
Reviewed the diff ( One real bug in the keyboard path:
Minor/architectural note (not blocking): Nothing else stood out — no XSS/injection risk (button text set via |
…cused Addresses the claude-review finding on #140. onKeydown is a window-CAPTURE listener; only the arrow branch checked isEditableFocus(), so a live document selection + Tab into the composer textarea meant Enter fired confirmSelection() instead of a newline. Bail on isEditableFocus() before the Enter/Escape branches. Adds a comment on the single-armed-host assumption and jsdom regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ey41dRwDpRgLtVHZjhYLKt
|
Reviewed the One correctness concern worth addressing before merge: Global keydown capture isn't scoped to the diff. Self-acknowledged multi-instance issue. The comment above the Everything else — the button positioning (coarse-pointer offset for iOS's native callout), the click-capture guard for drag-select, and the cleanup/sweep lifecycle mirroring |
Round-2 review follow-up (#140): the window-capture keydown listener swallowed Left/Right/Enter/Escape page-wide whenever a host was mounted and no form field was focused, so an unrelated arrow-key widget would lose its keys. Now handle keys only when the interaction belongs to the diff — nothing else focused (page-load/body, so the on-load caret stays drivable) or focus within the host. Regression tests added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ey41dRwDpRgLtVHZjhYLKt
|
Reviewed the diff (
Nothing blocking — the rune/offset math, side-boundary handling, and cleanup/sweep logic all look correct, and the new tests exercise the tricky parts (surrogate pairs, gutter exclusion, old/new boundary, keyboard focus gating) well. |
Adds
lvt-fx:text-select, a native-selection capture directive so a host app can let users comment on a word / phrase / multi-line span — not just whole lines. Mirrors the existingregion-selectarm/sweep/teardown lifecycle.What it does
textOffsetsFromSelection(exported, unit-tested): resolves a native selection to rune-offset{fromLine,fromCol,toLine,toCol,side,text}against[data-line]/[data-side]rows, using a[data-line-text]descendant as the gutter-excluding text container. Rejects cross-side and out-of-content boundaries.selectionchange→ floating Comment button, positioned below the selection on coarse pointers so iOS's native selection callout (an OS layer that beats z-index) doesn't cover it.←/→move the column,↑/↓follow the host's line cursor,Shift+arrowsextend a selection viagetSelection().modify()on the non-editable text (no contenteditable),Ctrl/Alt= word,Entercommits.Offsets are rune counts (matches Go's
[]rune), relying on the host keeping[data-line-text]'stextContentequal to the raw line. Styling is overridable via--lvt-text-select-*/--lvt-text-caret-*custom properties.Tests
tests/text-select.test.ts(jsdom): word / cross-token / multi-line / emoji-rune / gutter-exclusion / cross-side / collapsed / outside-host. Full suite green (781).🤖 Generated with Claude Code