Lunia DS

AI output

Code block

Code inside an AI answer that is highlighted while it streams in behind a caret, where wrapping and folding ease the block to its new height and Copy and Apply confirm in place with a drawn check and a morphing label.

Loading preview

A TypeScript file for a small invoice helper streams in line by line with a caret, then shows Copy, wrap and Apply; a second, longer Python script arrives complete and starts folded with "Show 24 more lines". Replay streams the first block again and folds the second.

Installation

Run this in your app after the one-time setup on the Getting started page.

$ npx @ganisigit/lunia-ds add code-block

Usage

The demo above, as code.

demo.tsx
'use client';

import * as React from 'react';
import { type ApplyStatus, CodeBlock } from './code-block';

const INVOICE = `export function formatInvoice(total: number) {
  const tax = total * 0.11;
  // round to two decimals
  return \`Total: \${(total + tax).toFixed(2)}\`;
}`;

const REPORT = `import csv
from pathlib import Path

SOURCE = Path("orders.csv")
REPORT = Path("report.txt")

def read_orders(path):
    with path.open() as file:
        return list(csv.DictReader(file))

def total_by_city(orders):
    totals = {}
    for order in orders:
        city = order["city"]
        amount = float(order["amount"])
        totals[city] = totals.get(city, 0) + amount
    return totals


def top_cities(totals, count=5):
    pairs = sorted(totals.items(), key=lambda p: p[1], reverse=True)
    return pairs[:count]


def write_report(rows, path):
    lines = ["City, Total"]
    for city, total in rows:
        lines.append(f"{city}, {total:.2f}")
    lines.append("")
    path.write_text("\\n".join(lines))


def main():
    orders = read_orders(SOURCE)
    totals = total_by_city(orders)
    write_report(top_cities(totals), REPORT)
    print("Report written")


# Run the report when the file is started.
if __name__ == "__main__":
    main()`;

const CHUNK = 5;
const TICK = 35;

export default function CodeBlockDemo({ replay = 0 }: { replay?: number }) {
  const [shown, setShown] = React.useState(INVOICE.length);
  const [apply, setApply] = React.useState<ApplyStatus>('idle');
  const timer = React.useRef(0);

  // Replay streams the first block in again, and mounts the second one again so that it starts folded.
  React.useEffect(() => {
    if (replay === 0) return;
    setApply('idle');
    setShown(0);
    timer.current = window.setInterval(() => {
      setShown((n) => {
        if (n + CHUNK >= INVOICE.length) {
          window.clearInterval(timer.current);
          return INVOICE.length;
        }
        return n + CHUNK;
      });
    }, TICK);
    return () => window.clearInterval(timer.current);
  }, [replay]);

  const streaming = shown < INVOICE.length;
  return (
    <div className="flex w-full max-w-[560px] flex-col gap-6">
      <CodeBlock
        code={INVOICE.slice(0, shown)}
        language="ts"
        filename="invoice.ts"
        streaming={streaming}
        applyStatus={apply}
        onApply={() => {
          setApply('loading');
          window.setTimeout(() => setApply('success'), 1100);
        }}
      />
      <CodeBlock key={replay} code={REPORT} language="python" filename="report.py" />
    </div>
  );
}

API

Parts: root (card), header: file name and language, wrap toggle, copy button (status button), apply button (status button, optional), code body: line numbers (sticky), highlighted lines, caret while streaming, fold fade (over folded code), fold bar: show or hide button with chevron.

PropTypeDefaultDescription
codestring-The code so far. Required. Update it as the stream delivers more.
languagestringundefinedLanguage id such as "ts", "python" or "bash"; shown in the header and used for comment rules.
filenamestringundefinedFile name shown in the header before the language.
streamingbooleanfalseTrue while the code is still arriving: shows the caret, never folds and waits to enable Apply.
lineNumbersbooleantrueShows line numbers.
collapseAfternumber18Folds code longer than this many lines plus 3; 0 never folds. Code you watched stream in stays open.
highlightbooleantrueTurns the built-in light highlighter on or off.
applyStatus"idle" | "loading" | "success" | "error""idle"Status of the apply action, set by the parent after apply. New code resets success and error to idle.
applyLabelstring"Apply"Idle label of the Apply button.
EventDescription
copyThe code was copied, or copying failed. Payload: { ok: boolean }.
applyThe person pressed Apply or Try again; the parent sets applyStatus. The Apply button shows only when this has a listener.
expandedChangeFolded code was shown or hidden. Payload: the new state.

Motion

Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.

IDTriggerElementPropertyFrom → toTimingReduced motion
CBK1While streamingcaret (after the last line)opacity blink (steps), repeats1 → 0duration-6xl (900)ease-linearNo loop
CBK2Press Copycopy buttonstatus button motion (check draws, label "Copied"; "Could not copy" on error); back to Copy 1600ms after Copied, 2400ms after an errorIdle → Success or Errorduration-xl (380)ease-outSwaps at once
CBK3Press Applyapply buttonstatus button motion ("Applying", check draws, "Applied"; "Try again" on error); after Applied it becomes the quiet ghost styleIdle → Success or Errorduration-xl (380)ease-outSwaps at once
CBK4Streaming endsapply buttonopacity0.4 → 1duration-lg (320)ease-outSwaps at once
CBK5Wrap toggled (code that does not fold)code bodyheightold height → new heightduration-3xl (460)ease-settleResizes at once
CBK6Show or hide folded linescode bodymax heightcollapseAfter lines → full heightduration-3xl (460)ease-settleResizes at once
CBK7Show or hide folded linesfold fadeopacity1 → 0duration-xl (380)ease-settleSwaps at once
CBK8Show or hide folded linesfold chevronrotation0deg → 180degduration-lg (320)ease-settleSwaps at once
CBK9Show or hide folded linesfold button labeltext morphShow 24 more lines → Hide 24 linesduration-3xl (460)ease-outSwaps at once
CBK10Hoverwrap toggle, fold buttonbackground color, text colortransparent, text-tertiary → bg-primary_hover, text-primaryduration-xs (160)ease-outSwaps at once

Accessibility

  • The code is a pre element named by its language and file name, for example "ts, invoice.ts"; when lines do not wrap it is focusable (tabIndex 0) so keyboard users can scroll it sideways.
  • Line numbers, the caret and the fold fade are aria-hidden; highlighting is color only and never carries meaning.
  • The wrap toggle is a toggle button named "Wrap lines" with aria-pressed.
  • The copy button is named "Copy code"; its status labels are read by the status button live region.
  • The fold button has aria-expanded and aria-controls pointing to the code body, and says how many lines it shows or hides.
  • Apply is disabled while streaming and says why with aria-describedby ("Available when the code is complete").
  • Header buttons are 28px with a visible focus ring (Focus rings/focus-ring).