Components/SelectionActions

SelectionActions

A floating bar that hands a text selection to an agent.

VerifiedSince 0.3.9

Usage

Rewriting a Selection

The host moves the bar through idle → thinking → streaming → result; the thinking label shimmers.

Loading demo...

Tracking the Selection

The component only presents; useSelectionAnchor() tracks selections in ordinary prose, and anywhere else you build a SelectionPayload yourself.

import { resolveSelectionPayload, useSelectionAnchor } from '@talex-touch/tuffex/selection-actions'

const { selection, clear } = useSelectionAnchor({
  root: articleRef,        // track selections inside this subtree only
  debounce: 120,           // selectionchange fires every frame while dragging
  minLength: 1,            // a selection too short to be worth a bar
  ignore: () => [barEl],   // focus landing on the bar is not a deselection
})

Best Practices

  • While streaming, call updatePosition() in the same frame as the text update, not through setTimeout.
  • Pass the instance's el to ignore: () => [barRef.value?.el ?? null]. document.querySelector returns the wrong bar once a page has two.
  • Call clear() after keep or discard, or the snapshot lingers and the bar never retracts.
  • Scope root to the article container, or any selection on the page pops the bar.
  • Use TxMessageActions for copy, regenerate, and speak at the foot of a message; this bar is only for rewriting a selection.

API Reference

Props

PropTypeDefaultDescription
selectionSelectionPayload | nullnull{ text, rects, range? } snapshot; null retracts the bar.
state'idle' | 'thinking' | 'streaming' | 'result''idle'The host owns the state machine; the component never calls a model.
actionsSelectionActionItem[]Explain / Improve plus folded Shorten / Tone / GrammarActions as { id, label, more?, busyLabel? }; give custom actions a busyLabel.
activeActionIdstring—Id of the running action; picks the busy wording.
expandedboolean—v-model:expanded, the folded action group.
promptstring—v-model:prompt, the free-text instruction.
hidePromptbooleanfalseHides the text field and send control, for read-only surfaces.
placeholderstring'Describe edits'Field placeholder, also its accessible name.
ariaLabelstring'Selection actions'Accessible name of the bar.
keepLabel / discardLabelstring'Keep' / 'Discard'The two result-state buttons.
retryLabelstring'Try again'Accessible name of the retry control.
sendLabelstring'Send edit instruction'Accessible name of the send control.
expandLabel / collapseLabelstring'Show more actions' / 'Show fewer actions'Accessible names of the expand control.
busyLabelstring'Editing'Fallback busy wording for an action without its own.
offsetnumber8Distance from the selection's last line, in px.

Events

EventArgumentsDescription
action({ id, action, selection })A preset action was pressed; carries the selection snapshot.
submit({ prompt, selection })Enter or send was pressed; prompt is trimmed, and a blank one never fires.
keep / discard()Keep or discard in the result state.
retry()Retry in the result state.
update:expanded(expanded: boolean)The folded group opened or closed.
update:prompt(prompt: string)The field's content changed.

Slots

SlotScopeDescription
action-icon{ action }Replaces an action's glyph; required for a custom id.
busy{ label }Replaces the busy readout.
result—Replaces the keep / discard / retry cluster.

Exposed Methods

MethodDescription
updatePosition()Repositions against the current selection.rects; a streaming host must call it.
focusInput()Focuses the free-text field.
elThe bar's root element, for useSelectionAnchor's ignore.

useSelectionAnchor Options

OptionTypeDefaultDescription
rootMaybeRefOrGetter<Element | null | undefined>—Confines tracking to one subtree; omit to watch the whole document.
debouncenumber120Settle time for selectionchange, in ms.
minLengthnumber1Minimum trimmed length.
disabledMaybeRefOrGetter<boolean>falseStops reporting without unmounting.
ignoreMaybeRefOrGetter<Array<Element | null | undefined>>[]Focus inside these elements doesn't count as a deselection.

Returns { selection, clear }: selection is a Ref<SelectionPayload | null>, and clear() is called once the rewrite is applied or dropped. resolveSelectionPayload() is its pure internal rule, exported for testing or custom tracking. Both are runtime functions; import them explicitly from @talex-touch/tuffex/selection-actions.

Overview

  • The anchor is the bottom of the selection's last line, centered on the whole selection. The bar never flips (disableFlip); it is only shifted back into the viewport horizontally.
  • The anchor is a virtual reference, so floating-ui won't follow text reflow; the host calls updatePosition() while streaming, and the component handles resize and scroll.
  • selection is a snapshot: focusing the field clears the browser selection, but actions still target the payload passed in.
  • The root swallows pointerdown, so pressing a button neither moves focus nor destroys the selection, except inside an input, textarea, or contenteditable. Safari and Firefox differ in caret behavior afterward; verify on real browsers.
  • The root is role="group", leaving arrow keys to the field's caret; folded actions, and the send control while prompt is empty, stay out of the tab order.
  • The entrance is a spring that overshoots its end value; under reduced motion it is dropped, the width tween is skipped, and the bar rests on its visible end state.

Technologies

  • The width morph runs through the Web Animations API, so the component reads prefers-reduced-motion in script; tracking builds on useTextSelection from @vueuse/core and clones the selection's range.
  • Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT; upstream has no real selection, so the tracking layer is new to this port.
  • Source: packages/tuffex/packages/components/src/selection-actions/.
查看源码
packages/tuffex/packages/components/src/selection-actions/index.ts