Components/CodeStream

CodeStream

A code block with a filename header that reveals line by line, with copy and syntax highlighting built in.

VerifiedSince 0.3.9

CodeStream

Basic Usage

Line-by-Line Reveal

The host advances revealedLines; the component owns the transition and the caret. The cadence (400ms before the first line, 240ms per line, a 3200ms hold) stays in the demo layer.

Loading demo...

Full Listing

Omit revealedLines and the whole listing shows, with no caret drawn.

Full listing

The settled state: filename, language label, copy, and highlighting.

Loading demo...

Highlighting and Theming

Highlighting runs through the repository's existing shiki runtime — the same lazy singleton TxCodeBlock uses — as a pure async enhancement: the plain-text rendering is always correct, and colour arrives when it arrives. With no lang, nothing is highlighted and shiki is never loaded.

theme defaults to 'auto' and follows the document root's data-theme or .dark. Upstream is a single-theme demo; hardcoding light would render dark-on-light inside a dark host.

The component splits shiki's output back into lines and pairs each with a number and the reveal animation. It checks the line count before splitting: if the highlighted result does not have the same number of lines as the source, the whole block falls back to plain text rather than shifting every number by one.

API

Props

PropTypeDefaultDescription
codestringThe source. Required.
langstring''Shiki language id. Empty renders unhighlighted.
filenamestringHeader filename, in the mono face.
langLabelstringLanguage label beside the filename, e.g. TypeScript.
revealedLinesnumberHow many lines are revealed. Omit or pass -1 for all.
caretbooleantrueDraws the accent caret after the last revealed line.
lineNumbersbooleantrueShows the line-number gutter.
theme'light' | 'dark' | 'auto''auto'Highlight theme; 'auto' follows the document root.
copyablebooleantrueRenders the copy button.
copyLabelstring'Copy'Copy button text.
copiedLabelstring'Copied'Text shown after a successful copy.
minHeightnumber | stringFloor for the code area. Defaults to the full listing's height.

Events

EventPayloadDescription
copy(code: string)Forwarded from TxCopyButton on a successful copy.
completeEmitted once when revealedLines reaches the last line. Not emitted if it starts complete.

Slots

NameScopeDescription
headerReplaces the filename and language label pair.
actionsInserts custom controls before the copy button.

Interaction Contract

  • revealedLines is clamped to [0, total lines]; out-of-range values are not an error.
  • The caret appears only while revealing (0 < revealedLines < total), at the end of the last revealed line. It is still: upstream reserves the blinking caret for streaming prose, and the code caret is a position marker.
  • complete fires once, on crossing the end. Mounting with the listing already complete is not a crossing, so nothing is emitted.
  • The copy control reuses TxCopyButton, so it inherits the document.execCommand fallback and the polite live region; only its chrome is restyled to match.
  • The gutter's line-height: 1.86 against the code's 1.7 is deliberate: 10.5px × 1.86 and 11.5px × 1.7 both land near 19.5px, which is what puts a number on its line's baseline. Changing either side breaks the pairing.
  • The height floor reserves lines × 1.7em + 20px, so a reveal grows into space already held rather than pushing the page down. Upstream's hardcoded 137px is exactly six lines of its own sample.
  • The code area is a <div>, not a <pre>: Vue's compiler preserves template whitespace inside <pre>, so the markup's own indentation would render as code. white-space: pre per line says the same thing.
  • Under reduced motion the per-line entrance stops while the reveal itself continues.

Best Practices

  • Let the host advance revealedLines at the pace data arrives; do not put a timer inside the component.
  • When the language is uncertain, prefer omitting lang: plain text is always correct, and a wrong id only costs a wasted highlight pass.
  • Pair long listings with an outer scroll container. The code area scrolls horizontally on its own, but its height follows content.
  • Use TxCodeBlock when a fence should match TxStreamMarkdown; use this component when you need a filename header, a gutter, and a line-by-line reveal.
  • In non-English UIs, override copyLabel and copiedLabel together — changing only one leaves mixed-language copy.

Source

  • Component source: packages/tuffex/packages/components/src/code-stream/src/TxCodeStream.vue.
  • Types: packages/tuffex/packages/components/src/code-stream/src/types.ts.
  • Reused: packages/tuffex/packages/components/src/button/src/copy-button.vue, packages/tuffex/packages/components/src/stream-markdown/src/shiki-runtime.ts, and .../use-auto-theme.ts.
  • Verified coverage: packages/tuffex/packages/components/src/code-stream/__tests__/code-stream.test.ts (22 cases) covers reveal clamping and reuse of already-revealed lines, caret conditions, complete firing once, header presence, splitting shiki output into lines, the plain-text fallback on a line-count mismatch, theme forwarding, and copy event forwarding.
  • Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT.
查看源码
packages/tuffex/packages/components/src/code-stream/index.ts