Skip to content
ilinxa/pro-ui

Code Block

alphav0.2.1

Code surface with view, edit, and terminal modes — Shiki highlighting, dual-theme CSS variables, and chrome presets for docs, chat, and terminal UIs.

Category: CodeUpdated: 2026-08-19Created: 2026-05-10Author: ilinxa

Context

Substrate for every 'render code professionally' surface in the library — chat assistants, fenced markdown blocks, JSON / config viewers, card-tree code sections, virtual terminal walkthroughs, and snippet editors. View mode uses Shiki's GitHub Light + GitHub Dark Default themes (toggled via the active `.dark` class with zero re-tokenize). Edit mode wraps a CodeMirror 6 instance with a custom HighlightStyle approximating the same GitHub palette (a near-match, not pixel-perfect — a true Shiki → CodeMirror theme bridge is not implemented). Terminal mode renders structured `TerminalLine[]` rows with prompt detection on `$ `, `> `, `# ` prefixes and macOS-style traffic-light decoration. Streaming-friendly via an explicit `streaming` flag that batches re-tokenization to rAF and shows a blinking tail cursor. Filename → lang derivation works out of the box for ~30 extensions; consumer can override via `filenameToLang`. Object-shape callbacks throughout (per F-cross-12).

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Install the component
pnpm dlx shadcn@latest add @ilinxa/code-block

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/code-block-fixtures

CLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:

"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}

Preview

1. View mode — TypeScript (the default)

Filename + language pill + copy button. GitHub Light / Dark token palette.

counter.tsxtsx
import { useState } from "react";import { Button } from "@/components/ui/button"; export default function Counter() {  const [n, setN] = useState(0);  return (    <div className="flex items-center gap-3">      <Button onClick={() => setN(n - 1)}>−</Button>      <span className="font-mono">{n}</span>      <Button onClick={() => setN(n + 1)}>+</Button>    </div>  );} 

2. Line highlights + annotations

Highlighted rows draw the eye; severity icons in the gutter open tooltips with messages.

counter.tsxtsx
import { useState } from "react";import { Button } from "@/components/ui/button"; export default function Counter() {  const [n, setN] = useState(0);  return (    <div className="flex items-center gap-3">      <Button onClick={() => setN(n - 1)}>−</Button>      <span className="font-mono">{n}</span>      <Button onClick={() => setN(n + 1)}>+</Button>    </div>  );} 

3. Streaming — chat assistant style

Click Replay to emit 10-char chunks at 50 ms intervals. Tail cursor blinks while streaming; tokenization stays smooth.

Click to start
counter.tsxtsx

4. JSON config + long-block collapse

Long blocks fade out with a 'Show all' button. Click to expand inline.

package.jsonjson
{  "type": "FeatureCollection",  "features": [    { "type": "Feature", "id": 1, "geometry": { "type": "Point", "coordinates": [-42.4923, -16.8769] } },    { "type": "Feature", "id": 2, "geometry": { "type": "Point", "coordinates": [95.0154, 56.2462] } },    { "type": "Feature", "id": 3, "geometry": { "type": "Point", "coordinates": [-127.4769, -50.6307] } },    { "type": "Feature", "id": 4, "geometry": { "type": "Point", "coordinates": [10.0308, 22.4924] } },    { "type": "Feature", "id": 5, "geometry": { "type": "Point", "coordinates": [147.5385, -84.3845] } },    { "type": "Feature", "id": 6, "geometry": { "type": "Point", "coordinates": [-74.9538, -11.2614] } },    { "type": "Feature", "id": 7, "geometry": { "type": "Point", "coordinates": [62.5539, 61.8617] } },    { "type": "Feature", "id": 8, "geometry": { "type": "Point", "coordinates": [-159.9384, -45.0152] } },    { "type": "Feature", "id": 9, "geometry": { "type": "Point", "coordinates": [-22.4307, 28.1079] } },    { "type": "Feature", "id": 10, "geometry": { "type": "Point", "coordinates": [115.0770, -78.7690] } },    { "type": "Feature", "id": 11, "geometry": { "type": "Point", "coordinates": [-107.4153, -5.6459] } },    { "type": "Feature", "id": 12, "geometry": { "type": "Point", "coordinates": [30.0924, 67.4772] } },    { "type": "Feature", "id": 13, "geometry": { "type": "Point", "coordinates": [167.6001, -39.3997] } },    { "type": "Feature", "id": 14, "geometry": { "type": "Point", "coordinates": [-54.8922, 33.7234] } },    { "type": "Feature", "id": 15, "geometry": { "type": "Point", "coordinates": [82.6155, -73.1535] } },    { "type": "Feature", "id": 16, "geometry": { "type": "Point", "coordinates": [-139.8768, -0.0304] } },    { "type": "Feature", "id": 17, "geometry": { "type": "Point", "coordinates": [-2.3691, 73.0927] } },    { "type": "Feature", "id": 18, "geometry": { "type": "Point", "coordinates": [135.1386, -33.7842] } },    { "type": "Feature", "id": 19, "geometry": { "type": "Point", "coordinates": [-87.3537, 39.3389] } },    { "type": "Feature", "id": 20, "geometry": { "type": "Point", "coordinates": [50.1540, -67.5380] } },    { "type": "Feature", "id": 21, "geometry": { "type": "Point", "coordinates": [-172.3383, 5.5851] } },    { "type": "Feature", "id": 22, "geometry": { "type": "Point", "coordinates": [-34.8306, 78.7082] } },    { "type": "Feature", "id": 23, "geometry": { "type": "Point", "coordinates": [102.6771, -28.1687] } },    { "type": "Feature", "id": 24, "geometry": { "type": "Point", "coordinates": [-119.8152, 44.9544] } },    { "type": "Feature", "id": 25, "geometry": { "type": "Point", "coordinates": [17.6925, -61.9225] } },    { "type": "Feature", "id": 26, "geometry": { "type": "Point", "coordinates": [155.2002, 11.2006] } },    { "type": "Feature", "id": 27, "geometry": { "type": "Point", "coordinates": [-67.2921, 84.3237] } },    { "type": "Feature", "id": 28, "geometry": { "type": "Point", "coordinates": [70.2156, -22.5532] } },    { "type": "Feature", "id": 29, "geometry": { "type": "Point", "coordinates": [-152.2767, 50.5699] } },    { "type": "Feature", "id": 30, "geometry": { "type": "Point", "coordinates": [-14.7690, -56.3070] } },    { "type": "Feature", "id": 31, "geometry": { "type": "Point", "coordinates": [122.7387, 16.8161] } },    { "type": "Feature", "id": 32, "geometry": { "type": "Point", "coordinates": [-99.7536, 89.9392] } },    { "type": "Feature", "id": 33, "geometry": { "type": "Point", "coordinates": [37.7541, -16.9377] } },    { "type": "Feature", "id": 34, "geometry": { "type": "Point", "coordinates": [175.2618, 56.1854] } },    { "type": "Feature", "id": 35, "geometry": { "type": "Point", "coordinates": [-47.2305, -50.6915] } },    { "type": "Feature", "id": 36, "geometry": { "type": "Point", "coordinates": [90.2772, 22.4316] } },    { "type": "Feature", "id": 37, "geometry": { "type": "Point", "coordinates": [-132.2151, -84.4453] } },    { "type": "Feature", "id": 38, "geometry": { "type": "Point", "coordinates": [5.2926, -11.3222] } },    { "type": "Feature", "id": 39, "geometry": { "type": "Point", "coordinates": [142.8003, 61.8009] } },    { "type": "Feature", "id": 40, "geometry": { "type": "Point", "coordinates": [-79.6920, -45.0760] } },    { "type": "Feature", "id": 41, "geometry": { "type": "Point", "coordinates": [57.8157, 28.0471] } },    { "type": "Feature", "id": 42, "geometry": { "type": "Point", "coordinates": [-164.6766, -78.8298] } },    { "type": "Feature", "id": 43, "geometry": { "type": "Point", "coordinates": [-27.1689, -5.7067] } },    { "type": "Feature", "id": 44, "geometry": { "type": "Point", "coordinates": [110.3388, 67.4164] } },    { "type": "Feature", "id": 45, "geometry": { "type": "Point", "coordinates": [-112.1535, -39.4605] } },    { "type": "Feature", "id": 46, "geometry": { "type": "Point", "coordinates": [25.3542, 33.6626] } },    { "type": "Feature", "id": 47, "geometry": { "type": "Point", "coordinates": [162.8619, -73.2143] } },    { "type": "Feature", "id": 48, "geometry": { "type": "Point", "coordinates": [-59.6304, -0.0912] } },    { "type": "Feature", "id": 49, "geometry": { "type": "Point", "coordinates": [77.8773, 73.0319] } },    { "type": "Feature", "id": 50, "geometry": { "type": "Point", "coordinates": [-144.6150, -33.8450] } },    { "type": "Feature", "id": 51, "geometry": { "type": "Point", "coordinates": [-7.1073, 39.2781] } },    { "type": "Feature", "id": 52, "geometry": { "type": "Point", "coordinates": [130.4004, -67.5988] } },    { "type": "Feature", "id": 53, "geometry": { "type": "Point", "coordinates": [-92.0919, 5.5243] } },    { "type": "Feature", "id": 54, "geometry": { "type": "Point", "coordinates": [45.4158, 78.6474] } },    { "type": "Feature", "id": 55, "geometry": { "type": "Point", "coordinates": [-177.0765, -28.2295] } },    { "type": "Feature", "id": 56, "geometry": { "type": "Point", "coordinates": [-39.5688, 44.8936] } },    { "type": "Feature", "id": 57, "geometry": { "type": "Point", "coordinates": [97.9389, -61.9833] } },    { "type": "Feature", "id": 58, "geometry": { "type": "Point", "coordinates": [-124.5534, 11.1398] } },    { "type": "Feature", "id": 59, "geometry": { "type": "Point", "coordinates": [12.9543, 84.2629] } },    { "type": "Feature", "id": 60, "geometry": { "type": "Point", "coordinates": [150.4620, -22.6140] } }  ]} 

5. Terminal — virtual install walkthrough

Structured `lines: TerminalLine[]` API. Input rows show the prompt; output rows are muted; error rows in destructive red. macOS traffic-light decoration optional.

zshbash
$ pnpm install
Resolving dependencies...
Adding 247 packages from 38 contributors...
✓ Done in 8.3s
$ pnpm dev
 
▲ Next.js 16.2.0 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.4:3000
 
✓ Ready in 1.1s
$ pnpm build
✗ Failed to compile
Type error: Property 'foo' does not exist on type 'Bar'

6. Edit mode — controlled CodeMirror editor

Same JetBrains Mono font, same line-height, near-match token colors via custom CodeMirror HighlightStyle. v0.2.0 will swap in a pixel-perfect Shiki bridge.

greet.tsts
Press Cmd/Ctrl+S to save.

7. Custom header — action slot

The header `actions` slot accepts any ReactNode; pre-built buttons (copy, wrap, expand) compose around it.

run-me.tsts
// click Run to evaluate (consumer-managed)console.log("hello from ilinxa");

8. Python sample — multi-language coverage

Shiki loads grammars on demand; common languages stay synchronously bundled (~10), the rest dynamic-import.

users.pypython
from dataclasses import dataclass @dataclassclass User:    id: int    name: str    email: str | None = None def greet(user: User) -> str:    if user.email:        return f"Hello {user.name} <{user.email}>"    return f"Hello {user.name}" users = [    User(id=1, name="Ada", email="ada@example.com"),    User(id=2, name="Linus"),] for u in users:    print(greet(u)) 

9. Error trace — annotations on a plain log

`lang='plaintext'` works as a viewer for arbitrary text. Combine with annotations + line-highlights for a debug surface.

stderr.log
TypeError: Cannot read property 'name' of undefined    at processUser (/app/src/users.ts:7:24)    at /app/src/index.ts:15:3    at Array.map (<anonymous>)    at greetAll (/app/src/index.ts:14:18)    at Object.<anonymous> (/app/src/index.ts:42:1)    at Module._compile (node:internal/modules/cjs/loader.js:1234:30) 

10. Inline JSON (no chrome)

`header={false}` for a minimal embed.

{  "name": "ilinxa-ui-pro",  "version": "0.1.0",  "private": true,  "dependencies": {    "next": "^16.2.0",    "react": "^19.2.0",    "shiki": "^4.0.2",    "@codemirror/state": "^6.6.0"  },  "scripts": {    "dev": "next dev",    "build": "next build",    "lint": "eslint"  }} 

Demo source

demo.tsxtsx
"use client";import { useEffect, useRef, useState } from "react";import { Button } from "@/components/ui/button";import { CodeBlock } from "./code-block";import {  SAMPLE_EDIT_DEFAULT,  SAMPLE_ERROR_ANNOTATIONS,  SAMPLE_ERROR_TRACE,  SAMPLE_JSON,  SAMPLE_LONG_JSON,  SAMPLE_PYTHON,  SAMPLE_TERMINAL,  SAMPLE_TS,  SAMPLE_TS_HIGHLIGHTED,  chunkString,} from "./dummy-data"; function Section({  title,  caption,  children,}: {  title: string;  caption?: string;  children: React.ReactNode;}) {  return (    <section className="flex flex-col gap-2">      <div className="flex flex-col gap-1">        <h3 className="text-sm font-medium text-foreground">{title}</h3>        {caption ? (          <p className="text-xs text-muted-foreground">{caption}</p>        ) : null}      </div>      {children}    </section>  );} function StreamingDemo() {  const [value, setValue] = useState("");  const [streaming, setStreaming] = useState(false);  const timeoutsRef = useRef<number[]>([]);   const replay = () => {    // Cancel anything in flight    for (const id of timeoutsRef.current) window.clearTimeout(id);    timeoutsRef.current = [];    setValue("");    setStreaming(true);    const chunks = chunkString(SAMPLE_TS, 10);    let acc = "";    chunks.forEach((c, i) => {      const id = window.setTimeout(() => {        acc += c;        setValue(acc);        if (i === chunks.length - 1) {          // Final tokenize on stop          window.setTimeout(() => setStreaming(false), 80);        }      }, i * 50);      timeoutsRef.current.push(id);    });  };   useEffect(() => {    return () => {      for (const id of timeoutsRef.current) window.clearTimeout(id);    };  }, []);   return (    <div className="flex flex-col gap-2">      <div className="flex items-center gap-2">        <Button size="sm" variant="outline" onClick={replay}>          Replay streaming        </Button>        <span className="text-xs text-muted-foreground">          {streaming ? "Streaming…" : value ? "Done" : "Click to start"}        </span>      </div>      <CodeBlock        lang="tsx"        filename="counter.tsx"        value={value}        streaming={streaming}        showCopy      />    </div>  );} function EditDemo() {  const [code, setCode] = useState(SAMPLE_EDIT_DEFAULT);  const [saved, setSaved] = useState<string | null>(null);   return (    <div className="flex flex-col gap-2">      <CodeBlock        mode="edit"        lang="ts"        filename="greet.ts"        value={code}        onChange={({ value }) => setCode(value)}        onSave={({ value }) => setSaved(value)}        showCopy      />      <div className="flex items-center gap-3 text-xs text-muted-foreground">        <span>Press Cmd/Ctrl+S to save.</span>        {saved ? (          <span className="text-foreground">            ✓ Saved {saved.length} chars          </span>        ) : null}      </div>    </div>  );} export default function CodeBlockDemo() {  return (    <div className="flex flex-col gap-10">      <Section        title="1. View mode — TypeScript (the default)"        caption="Filename + language pill + copy button. GitHub Light / Dark token palette."      >        <CodeBlock lang="tsx" filename="counter.tsx" value={SAMPLE_TS} />      </Section>       <Section        title="2. Line highlights + annotations"        caption="Highlighted rows draw the eye; severity icons in the gutter open tooltips with messages."      >        <CodeBlock          lang="tsx"          filename="counter.tsx"          value={SAMPLE_TS}          highlightedLines={SAMPLE_TS_HIGHLIGHTED}          annotations={[            { line: 5, type: "info", message: "Initial state is 0" },            { line: 9, type: "warn", message: "Consider a setter callback" },          ]}          showLineNumbers        />      </Section>       <Section        title="3. Streaming — chat assistant style"        caption="Click Replay to emit 10-char chunks at 50 ms intervals. Tail cursor blinks while streaming; tokenization stays smooth."      >        <StreamingDemo />      </Section>       <Section        title="4. JSON config + long-block collapse"        caption="Long blocks fade out with a 'Show all' button. Click to expand inline."      >        <CodeBlock          lang="json"          filename="package.json"          value={SAMPLE_LONG_JSON}          showLineNumbers          maxLines={12}        />      </Section>       <Section        title="5. Terminal — virtual install walkthrough"        caption="Structured `lines: TerminalLine[]` API. Input rows show the prompt; output rows are muted; error rows in destructive red. macOS traffic-light decoration optional."      >        <CodeBlock          mode="terminal"          filename="zsh"          showTrafficLights          lines={SAMPLE_TERMINAL}        />      </Section>       <Section        title="6. Edit mode — controlled CodeMirror editor"        caption="Same JetBrains Mono font, same line-height, near-match token colors via custom CodeMirror HighlightStyle. v0.2.0 will swap in a pixel-perfect Shiki bridge."      >        <EditDemo />      </Section>       <Section        title="7. Custom header — action slot"        caption="The header `actions` slot accepts any ReactNode; pre-built buttons (copy, wrap, expand) compose around it."      >        <CodeBlock          lang="ts"          filename="run-me.ts"          value={`// click Run to evaluate (consumer-managed)\nconsole.log("hello from ilinxa");`}          showCopy          showWrap          actions={            <Button size="sm" variant="outline" className="h-7">              ▶ Run            </Button>          }        />      </Section>       <Section        title="8. Python sample — multi-language coverage"        caption="Shiki loads grammars on demand; common languages stay synchronously bundled (~10), the rest dynamic-import."      >        <CodeBlock          lang="python"          filename="users.py"          value={SAMPLE_PYTHON}          showLineNumbers          showWrap          showExpand        />      </Section>       <Section        title="9. Error trace — annotations on a plain log"        caption="`lang='plaintext'` works as a viewer for arbitrary text. Combine with annotations + line-highlights for a debug surface."      >        <CodeBlock          lang="plaintext"          filename="stderr.log"          value={SAMPLE_ERROR_TRACE}          highlightedLines={[1]}          annotations={SAMPLE_ERROR_ANNOTATIONS}          showLineNumbers        />      </Section>       <Section        title="10. Inline JSON (no chrome)"        caption="`header={false}` for a minimal embed."      >        <CodeBlock lang="json" value={SAMPLE_JSON} header={false} />      </Section>    </div>  );} 

Usage

When to use

CodeBlockis the substrate for every “render code professionally” surface in the library: chat assistants, fenced markdown blocks, JSON/config viewers, card-tree “code” sections, virtual terminal walkthroughs, and snippet editors. Three modes (view / edit / terminal) cover the common cases. Streaming-friendly. Language-agnostic.

Basic — view

import { CodeBlock } from "@/components/code-block";

<CodeBlock filename="app.tsx" value={code} />

Edit mode (controlled)

<CodeBlock
  mode="edit"
  lang="ts"
  filename="greet.ts"
  value={code}
  onChange={({ value }) => setCode(value)}
  onSave={({ value }) => save(value)}
/>

Streaming (chat assistant)

<CodeBlock
  lang="ts"
  value={partial}
  streaming={isStillStreaming}
/>

Terminal

<CodeBlock
  mode="terminal"
  showTrafficLights
  lines={[
    { kind: "input",  text: "$ pnpm install" },
    { kind: "output", text: "Resolving... done" },
    { kind: "error",  text: "ENOENT: no such file" },
  ]}
/>

Content-Security-Policy (read this before self-hosting)

The default highlighter engine is WebAssembly. Under a strict CSP your script-src must include 'wasm-unsafe-eval', or the browser refuses the compile outright (Wasm code generation disallowed by embedder) and highlighting can never run — no component can override its host's CSP. Either widen the policy, or switch engines:

// No WebAssembly at all — for CSP-restricted hosts.
// Slightly narrower grammar coverage is the trade.
<CodeBlock value={code} lang="ts" regexEngine="javascript" />

Highlighters are cached per engine, so mixing both on one page is safe. Either way the block never goes blank: if highlighting is unavailable the raw code renders as plain text and the body is marked data-highlight="failed".

Notes

  • Filename → lang priority: lang (if set) wins; else filenameToLang consumer override; else built-in extension map; else plaintext.
  • Streaming: explicit streaming prop. Final clean-tokenize fires when you flip it back to false. Don't auto-detect from update frequency.
  • RSC posture: the v0.1.0 client variant SSRs the first paint fine in Next.js. A dedicated zero-client-Shiki /server export defers to a later minor.
  • Edit ↔ view continuity: near-match in v0.1.0 (custom CodeMirror HighlightStyle approximating GitHub Light + Dark Default). Pixel-perfect Shiki bridge defers to a later minor.
  • Diff: use lang="diff" for unified diff text in v0.1.0; split-view will be a sibling code-diff component.
  • Soft-failure: a failed editor mount renders an inline error plus a Reload as view-only control (labels editorFailed / reloadAsViewOnly); a failed highlight renders the raw code. Neither ever leaves an empty panel.
  • Long blocks: opt-in via maxLines; renders a fade-out + “Show all (N more lines)” button.

Features

  • Three render modes (view / edit / terminal) in one component, switched by `mode` prop
  • Shiki tokenization for view mode (GitHub Light + Dark Default by default; consumer overridable)
  • CodeMirror 6 in edit mode with custom HighlightStyle for near-match view/edit visual continuity
  • Streaming-friendly: explicit `streaming` prop, rAF-batched re-tokenization, blinking tail cursor
  • Terminal mode with `lines: TerminalLine[]` API (input / output / error kinds) + prompt detection + optional macOS traffic-lights
  • Chrome: filename pill, language label, copy button (with success animation), expand-to-modal, wrap toggle, download button — all gated by `show*` flags
  • Body: optional line numbers (default off in view), wrap or scroll, highlighted line ranges, severity-icon annotations with tooltips, long-block collapse with 'Show all'
  • Filename → lang priority chain: `lang` prop > consumer `filenameToLang` > built-in 30-entry extension map > plaintext
  • Dual-theme via CSS variables: `.dark` class toggles palette with zero re-tokenize
  • Standalone header parts exported (`<CodeBlockCopyButton>`, `<CodeBlockTrafficLights>`, etc.) for `renderHeader` slot composition
  • Object-shape callbacks throughout (per F-cross-12)
  • Imperative handle: `copy()`, `focus()`, `getValue()`, and `scrollToLine()` — which works in all three modes (CodeMirror dispatch in edit, row targeting in view/terminal) and expands a collapsed block rather than scrolling to a row hidden under the fade

Tags

code-blocksyntax-highlightshikicodemirrorterminalchatmarkdowneditorviewerstreaming

Dependencies

shadcn primitives: button, dialog, tooltip
npm peer deps: shiki@^4.0.2, @codemirror/state@^6.6.0, @codemirror/view@^6.41.1, @codemirror/commands@^6.10.3, @codemirror/language@^6.12.3, @codemirror/autocomplete@^6.20.1, @lezer/highlight@^1.2.3, @codemirror/lang-javascript@^6.2.5, @codemirror/lang-json@^6.0.2, @codemirror/lang-python@^6.2.1, @codemirror/lang-html@^6.4.11, @codemirror/lang-css@^6.3.1, @codemirror/lang-markdown@^6.5.0, lucide-react@^1.11.0