Components/DiffTable

DiffTable

Presents an AI-proposed edit as a change set, tinting outgoing rows and expanding incoming ones stage by stage.

VerifiedSince 0.3.9

Usage

DiffTable

Proposed menu cleanup

Plays once on mount and rests on the completed diff; the replay button runs reset() + play().

Loading demo...

Best Practices

  • Express meaning through change rather than painting colours with custom classes; the tint, the strike-through, and the reveal all follow from it.
  • Set tintText: false on badge or coloured-chip columns, or the change tone repaints them wholesale.
  • Give columns percentage or fixed pixel widths so the appended row cannot drift out of alignment.
  • Use play="settled" for docs, snapshot tests, and anywhere the animation is unwanted — it hands you the finished state directly.
  • For button-triggered playback use play="manual" with reset() + play(); do not remount the component with a changing :key.

API Reference

Props

PropTypeDefaultDescription
columnsDiffTableColumn[][]Column configuration
rowsDiffTableRow[][]Rows, each carrying its own change kind
titlestring-Card bar heading; omit it and the whole bar is dropped
play'auto' | 'manual' | 'settled''auto'Playback mode
stageDelays[number, number, number][800, 1000, 1000]The three stage lengths, in milliseconds
durationnumber400Tween length for the tint and the reveal, in milliseconds
selectablebooleanfalseRender an accept control on every changed row, and make the row itself toggle it
modelValue(string | number)[]-v-model — keys of the accepted rows. Bound, the component is fully controlled; omitted, it tracks toggles internally and a row streamed in later arrives accepted
footerbooleanfalseRender the summary footer (counts left, apply button right)
hintstring-Hint at the right end of the title bar, e.g. "Click changed rows to toggle"
summaryFormatter(counts: DiffTableCounts) => stringsee belowFooter summary. Defaults to 2 removals · 1 addition
applyLabelFormatter(count: number) => stringsee belowApply button label. Defaults to Apply N changes
rowToggleLabelFormatter(accepted, change) => stringsee belowAccessible name of a row control. Defaults to Accept / Reject this change

All three formatters ship English defaults: TuffEx carries no message catalog, so pluralisation and translation belong to the host.

DiffTableCounts

FieldTypeDescription
added / removed / modifiednumberCounts accepted rows only, so the summary reports what pressing apply would do rather than what the diff proposed
totalnumberSum of the three

DiffTableColumn

FieldTypeDescription
keystringColumn key; also names the cell-<key> slot
titlestringHeader text
dataIndexstringField read from row.data; defaults to key
widthstring | numberTrack width; numbers are pixels, strings pass through ('34%')
align'left' | 'center' | 'right'Text alignment
strikeOnRemovebooleanStrikes this column through on removed rows — for the value being retired
tintTextbooleanWhether the text follows the change tone; defaults to true. Set false on columns that carry their own colour
format(value, row, index) => stringDefault text formatting

DiffTableRow

FieldTypeDescription
keystring | numberRow identity
dataTThe record itself
change'unchanged' | 'added' | 'removed' | 'modified'Change kind, defaulting to 'unchanged'; modified uses the warning tone

Events

EventPayloadDescription
stageChange(stage: number)Fires on every stage transition
settled()Fires once the final stage is reached, whatever route got it there
update:modelValue(keys)The accepted set changed. Emitted in row order, not toggle order
toggle({ key, accepted })One row was accepted or rejected
apply(keys)The apply button was pressed, with the keys still accepted at that moment

Slots

NameDescription
titleReplaces the card bar heading
hintReplaces the hint at the right end of the title bar
footerReplaces the whole footer; receives { counts, accepted }
cell-<columnKey>Custom cell; receives { row, column, value, change, index }, so the slot can react to the row's own state

Expose

NameDescription
play()Runs the sequence from wherever it currently rests
reset()Returns to the plain table and stops any pending stage
settle()Jumps straight to the completed diff
stageCurrent stage index; equals stageDelays.length once settled

Stages And Playback Modes

stageDelays is a three-part timeline of [hold, tint, expand], defaulting to [800, 1000, 1000]. The first segment is a deliberate reading pause: nothing moves until it and the second have elapsed (1.8s at the defaults), so a reader takes in the original data before the edit lands.

StageOn screen
0–1Every row plain
2removed / modified rows tint, recolour, and strike through
3 (terminal)added rows expand from 0fr to 1fr

play decides who drives it: auto plays once on mount and rests on the completed diff; manual stays plain until play() is called; settled renders the finished state immediately and registers no timers at all (for docs and tests).

Overview

  • The stage machine is this component's semantics, not demo choreography: the host sets the pace through play and the exposed methods, and describes the edit through rows[].change.
  • Timers are cleared in onBeforeUnmount; play="settled" registers none at all.
  • Reduced motion (prefers-reduced-motion: reduce) drops the tweens, never the state machine: stages still advance, they simply stop animating. Freezing the machine would leave the reader looking at a table that never shows the edit.
  • Row tints are class-driven rather than inline, so a tinted row still gives hover feedback.
  • A collapsed appended row carries aria-hidden and inert, so it is neither announced nor in the tab order.
  • The appended row's inner grid and the <colgroup> are both derived from columns — one source of truth.

Technologies

  • Component source: packages/tuffex/packages/components/src/diff-table/src/TxDiffTable.vue.
  • Types: packages/tuffex/packages/components/src/diff-table/src/types.ts exports DiffTableProps, DiffTableColumn, DiffTableRow, DiffChangeKind, DiffTablePlay, and DiffTableEmits.
  • Instance: packages/tuffex/packages/components/src/diff-table/index.ts writes TxDiffTableInstance out by hand — a generic component's expose surface is typed unwrapped, so stage is a number rather than a Ref<number>.
  • Test coverage: packages/tuffex/packages/components/src/diff-table/__tests__/diff-table.test.ts has 15 cases using fake timers, covering the three-stage timeline, stageChange / settled emissions, all three playback modes, the three exposed methods, per-column tint and strike, grid-and-colgroup agreement, the collapsed row's aria-hidden / inert, unmount cleanup, and play mode switching.
查看源码
packages/tuffex/packages/components/src/diff-table/index.ts
  • Provenance: Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT.
  • Divergence from upstream: Added rows render at their position in rows rather than being pinned last as upstream hardcodes, which preserves diff ordering and supports more than one addition; modified is new to this port and has no upstream counterpart.
  • Upstream defect fixed: Upstream tints outgoing rows with an inline style.background, which outranks its own hover rule and leaves exactly the rows a reader wants to inspect without hover feedback. This port drives the tint from a class instead.
  • Dark theme: Red and green fills come from --tx-bui-*-tint (translucent overlays), rather than the opaque --tx-color-success-light-9 family, so the tint reads the same on any surface the table sits on.
  • Known limitation: Under high-contrast themes (html[data-tx-contrast='high']) the --tx-bui-* tokens keep their upstream values and do not join the high-contrast ramp.