← ui

code

ui

a code block with syntax highlighting by shiki on the server: marks, diffs, copy

npx shadcn add @ag/code
Already installed? Preview an update
npx shadcn add @ag/code --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-10-15
  • lib/code-tokens: code's highlighter as one ink per character (codeColours, runsOf) for a view drawing text that is not all there yet, and useCodeColours, the same read in the browser. CODE_THEMES is the one spelling of code's two themes.
  • codeColours runs on Shiki's core with its JavaScript regex engine, loading only the grammar and the two themes asked for: a page colouring code in the browser no longer downloads Oniguruma's WebAssembly (about 600 KB, 226 KB compressed). useCodeColours takes given colours read ahead, and then loads nothing.
Updated 2026-10-15 · release notes

also called syntax highlighting

shelf text & code

uses copy, scrims, material

used by markdown, terminal-editor, ide

Run pnpm build from your project directory.

npx shadcn add @ag/code
app/page.tsx
import { Scrims } from "@/components/ui/scrims"

export default function Page() {
  return (
    <div className="px-6">
      {/* fixed to the viewport, so it lives outside the flow */}
      <Scrims top={false} />
      <article>…</article>
    </div>
  )
}

in glass

app/page.tsx
import { Scrims } from "@/components/ui/scrims"

export default function Page() {
  return (
    <div className="px-6">
      {/* fixed to the viewport, so it lives outside the flow */}
      <Scrims top={false} />
      <article>…</article>
    </div>
  )
}

A line is marked with a [!code highlight] comment on the line before it. The mark runs the full width and keeps a bar in the gutter, which is the part that survives scrolling a long line sideways.

clampPan.ts
const b = panBounds(view, fitted, band)
return { x: clamp(view.x, -b.x, b.x), y: clamp(view.y, -b.y, b.y), s: view.s }

A diff is [!code ++] and [!code --]. The sign is a real character in the gutter, not just a colour: colour alone is invisible to a reader who cannot separate red from green.

lightbox-wheel.ts
const b = panBounds(ctx.pose, ctx.fitted, ctx.band)
const dx = boundTick(wheelPx(input.deltaX, input.deltaMode, ctx.band.h))
w = { ...w, x: w.x - dx, y: w.y - dy } 
const x = clamp(w.raw0.x + w.x - dx, -b.x, b.x) 
const y = clamp(w.raw0.y + w.y - dy, -b.y, b.y) 
w = { ...w, x: x - w.raw0.x, y: y - w.raw0.y } 
effects.push({
  kind: "pose",
  pose: { ...ctx.pose, x: overshoot(w.raw0.x + w.x, b.x) }, 
  pose: { ...ctx.pose, x, y }, 
})

[!code focus] steps the rest of the block back. Hover to bring it all forward: a reader who wants the surrounding lines should not have to leave the page for them.

swipe.js
function swipeSlides(travel, slideW) {
  assert(slideW > 0, "a slide has no width")
  return Math.floor(Math.abs(travel) / slideW + 1 - SWIPE_COMMIT + 1e-9) 
}

[!code word:momentum] marks a word wherever it appears on the lines below.

ts
// The momentum is the platform's; only the momentum knows when fingers left.

For source you do not own, and so cannot write a comment into, highlight="2,5-6" takes the lines from outside. This page uses it: an item page renders its demo file verbatim and must not edit it to document it.

app/page.tsx
import { Scrims } from "@/components/ui/scrims"

export default function Page() {
  return (
    <div className="px-6">
      {/* fixed to the viewport, so it lives outside the flow */}
      <Scrims top={false} />
      <article>…</article>
    </div>
  )
}

A shell block, with no filename: the tab falls back to the language.

bash
npx shadcn add @ag/code
# the copy control comes with it

the demo above, verbatim

code.demo.tsx
import { Sample } from "@/app/samples"
import { Code } from "@/registry/base-nova/ui/code"
import { InlineCode } from "@/registry/base-nova/ui/code-inline"

const PLAIN = `import { Scrims } from "@/components/ui/scrims"

export default function Page() {
  return (
    <div className="px-6">
      {/* fixed to the viewport, so it lives outside the flow */}
      <Scrims top={false} />
      <article>…</article>
    </div>
  )
}`

// The notations live in the source, so they move with the line they mark when the
// code is edited. Shiki strips them from the render; the clipboard keeps the source.
const MARKED = `const b = panBounds(view, fitted, band)
// [!code highlight]
return { x: clamp(view.x, -b.x, b.x), y: clamp(view.y, -b.y, b.y), s: view.s }`

// The real commit: the wheel pan stopped rubbering and started clamping.
const DIFF = `const b = panBounds(ctx.pose, ctx.fitted, ctx.band)
const dx = boundTick(wheelPx(input.deltaX, input.deltaMode, ctx.band.h))
w = { ...w, x: w.x - dx, y: w.y - dy } // [!code --]
const x = clamp(w.raw0.x + w.x - dx, -b.x, b.x) // [!code ++]
const y = clamp(w.raw0.y + w.y - dy, -b.y, b.y) // [!code ++]
w = { ...w, x: x - w.raw0.x, y: y - w.raw0.y } // [!code ++]
effects.push({
  kind: "pose",
  pose: { ...ctx.pose, x: overshoot(w.raw0.x + w.x, b.x) }, // [!code --]
  pose: { ...ctx.pose, x, y }, // [!code ++]
})`

const FOCUS = `function swipeSlides(travel, slideW) {
  assert(slideW > 0, "a slide has no width")
  return Math.floor(Math.abs(travel) / slideW + 1 - SWIPE_COMMIT + 1e-9) // [!code focus]
}`

const WORDS = `// [!code word:momentum]
// The momentum is the platform's; only the momentum knows when fingers left.`

const SH = `npx shadcn add @ag/code
# the copy control comes with it`

const Note = ({ children }: { children: React.ReactNode }) => (
  <p className="max-w-prose text-[0.9375rem] leading-relaxed text-foreground/70">
    {children}
  </p>
)

export default function Demo() {
  return (
    <div className="space-y-12">
      <Sample name="inline" glance title="inline code, copyable commands">
        <div className="space-y-4">
          <p className="text-sm text-foreground/80">
            Run <InlineCode>pnpm build</InlineCode> from your project directory.
          </p>
          <Code compact lang="sh">
            npx shadcn add @ag/code
          </Code>
        </div>
      </Sample>
      <Code lang="tsx" filename="app/page.tsx">
        {PLAIN}
      </Code>

      <Sample name="glass" title="in glass">
        <div className="rounded-xl bg-[url(/lightbox/shore.jpg)] bg-cover bg-center p-6">
          <Code lang="tsx" filename="app/page.tsx" material="glass">
            {PLAIN}
          </Code>
        </div>
      </Sample>

      <div className="space-y-3">
        <Note>
          A line is marked with a{" "}
          <code className="font-mono text-xs">[!code highlight]</code> comment
          on the line before it. The mark runs the full width and keeps a bar in
          the gutter, which is the part that survives scrolling a long line
          sideways.
        </Note>
        <Code lang="ts" filename="clampPan.ts">
          {MARKED}
        </Code>
      </div>

      <div className="space-y-3">
        <Note>
          A diff is <code className="font-mono text-xs">[!code ++]</code> and{" "}
          <code className="font-mono text-xs">[!code --]</code>. The sign is a
          real character in the gutter, not just a colour: colour alone is
          invisible to a reader who cannot separate red from green.
        </Note>
        <Code lang="ts" filename="lightbox-wheel.ts">
          {DIFF}
        </Code>
      </div>

      <div className="space-y-3">
        <Note>
          <code className="font-mono text-xs">[!code focus]</code> steps the
          rest of the block back. Hover to bring it all forward: a reader who
          wants the surrounding lines should not have to leave the page for
          them.
        </Note>
        <Code lang="js" filename="swipe.js">
          {FOCUS}
        </Code>
      </div>

      <div className="space-y-3">
        <Note>
          <code className="font-mono text-xs">[!code word:momentum]</code> marks
          a word wherever it appears on the lines below.
        </Note>
        <Code lang="ts">{WORDS}</Code>
      </div>

      <div className="space-y-3">
        <Note>
          For source you do not own, and so cannot write a comment into,{" "}
          <code className="font-mono text-xs">highlight="2,5-6"</code> takes the
          lines from outside. This page uses it: an item page renders its demo
          file verbatim and must not edit it to document it.
        </Note>
        <Code lang="tsx" filename="app/page.tsx" highlight="2,5-6" lines>
          {PLAIN}
        </Code>
      </div>

      <div className="space-y-3">
        <Note>
          A shell block, with no filename: the tab falls back to the language.
        </Note>
        <Code lang="bash">{SH}</Code>
      </div>
    </div>
  )
}

the source, every file an install writes

code.tsx
// A code block, highlighted on the SERVER. Shiki is the highlighter VS Code uses, so
// the grammars and themes are the ones the reader already knows, and running it at
// build time means the page ships the markup and none of the highlighter: only the
// copy control is client code.
//
// Two themes are baked into one render (`--shiki-light` / `--shiki-dark` on every
// token) and code.css picks with the theme, so switching does not re-highlight and
// cannot flash.

import "server-only"
import {
  transformerNotationDiff,
  transformerNotationErrorLevel,
  transformerNotationFocus,
  transformerNotationHighlight,
  transformerNotationWordHighlight,
} from "@shikijs/transformers"
import { type BundledLanguage, codeToHtml } from "shiki"
import { CODE_THEMES, linesOf } from "@/registry/base-nova/lib/code-tokens"
import { Copy } from "@/registry/base-nova/ui/copy"
import { Glass } from "@/registry/base-nova/ui/material-glass"
import "./code.css"
import "./edge-fade"

export interface CodeProps {
  /** The source. Common leading indentation is removed, so a snippet can be written
   *  indented inside the code that renders it and still read flush. */
  children: string
  /** A highlighted command row without a filename bar. Copy stays on the right;
   *  a command too long for the row scrolls on one line, its hidden side fading. */
  compact?: boolean
  /** A Shiki language id. `text` when it is not code, which skips the grammar. */
  lang?: BundledLanguage | "text"
  /** A path or a name for the tab. Without one the frame is bare. */
  filename?: string
  /** Hide the copy control, for a snippet nobody would want in their clipboard. */
  copy?: boolean
  /** Lines to mark, `"2"` or `"1,4-6"`, counted from 1. For source you do NOT own:
   *  a file rendered verbatim cannot carry `[!code highlight]` comments without them
   *  becoming part of the file. Where you do own it, prefer the comments, which move
   *  with the lines when the code is edited. */
  highlight?: string
  /** Number the lines. Off by default: most snippets are short enough that numbers
   *  are furniture, and they are only worth their width when something refers to one. */
  lines?: boolean
  /** Read `[!code ...]` comments. ON, except where the source is being shown AS
   *  ITSELF: a file that writes those comments inside a string would have them eaten
   *  out of its own listing, and a block promising verbatim would be quietly lying. */
  notations?: boolean
  /** Its face: `plain`, the page's frame (default), or `glass`, the one glass
   *  material (`material-glass`), its own border and ground dropped for it. Set
   *  here rather than read from a provider: the block renders on the server. */
  material?: "plain" | "glass"
  /** No frame, no bar and no copy of its own: for a frame that holds it (a
   *  tab of a `Panel` with an example's code, its copy in the panel's head), so a
   *  block in a frame is never a frame in a frame. `material` is then the
   *  holder's. */
  bare?: boolean
  className?: string
}

/** Strip the shortest indentation any non-blank line has, and the blank first and
 *  last lines a template literal always brings. */
function dedent(src: string): string {
  const lines = src.replace(/\t/g, "  ").split("\n")
  while (lines.length && !lines[0]?.trim()) lines.shift()
  while (lines.length && !lines[lines.length - 1]?.trim()) lines.pop()
  const pad = Math.min(
    ...lines
      .filter((l) => l.trim())
      .map((l) => l.length - l.trimStart().length),
  )
  return lines.map((l) => l.slice(pad)).join("\n")
}

export async function Code({
  children,
  compact = false,
  lang = "tsx",
  filename,
  copy = true,
  highlight,
  lines,
  notations = true,
  material = "plain",
  bare = false,
  className,
}: CodeProps) {
  const source = dedent(children)
  const marked = highlight ? linesOf(highlight) : null
  const html = await codeToHtml(source, {
    lang,
    themes: CODE_THEMES,
    defaultColor: false,
    transformers: [
      // The notations travel INSIDE the source, so they survive an edit that moves a
      // line and they are what a reader copies out of a doc. Shiki strips the comment
      // from the rendered output; the clipboard gets `source`, which still has it.
      ...(notations
        ? [
            transformerNotationDiff({ matchAlgorithm: "v3" }),
            transformerNotationHighlight({ matchAlgorithm: "v3" }),
            transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
            transformerNotationFocus({ matchAlgorithm: "v3" }),
            transformerNotationErrorLevel({ matchAlgorithm: "v3" }),
          ]
        : []),
      // ...and the `highlight` prop for source nobody may write into.
      {
        name: "ag:highlight-prop",
        line(node, line) {
          if (marked?.has(line)) this.addClassToHast(node, "highlighted")
        },
      },
    ],
  })
  const body = (
    <>
      {!compact && (
        <div className="ag-code-bar">
          <span className="ag-code-name">{filename ?? lang}</span>
          {copy && <Copy value={source} label />}
        </div>
      )}
      {/* Shiki's own <pre><code>, styled by code.css. A long line scrolls sideways,
          and a block given less height than it needs scrolls down, inside the frame,
          with the scrims' edge fade: never a scrollbar drawn over the code, never a
          hard cut. */}
      <div
        className="ag-code-scroll ag-edge-fade-xy"
        // biome-ignore lint/security/noDangerouslySetInnerHtml: shiki's own output, built on the server from a string prop, never from anything a reader can reach
        dangerouslySetInnerHTML={{ __html: html }}
      />
      {compact && copy && <Copy value={source} className="mr-2" />}
    </>
  )
  const frame = `ag-code${lines ? " ag-code-numbered" : ""}${className ? ` ${className}` : ""}`
  if (bare)
    return (
      <div data-slot="code" data-bare className={frame}>
        <div
          className="ag-code-scroll ag-edge-fade-xy"
          // biome-ignore lint/security/noDangerouslySetInnerHtml: shiki's own output, built on the server from a string prop, never from anything a reader can reach
          dangerouslySetInnerHTML={{ __html: html }}
        />
      </div>
    )
  if (material === "glass")
    return (
      <Glass
        shape="surface"
        data-slot="code"
        data-compact={compact || undefined}
        className={`${frame} [--glass-radius:0.75rem]`}
      >
        {body}
      </Glass>
    )
  return (
    <div
      data-slot="code"
      data-material="plain"
      data-compact={compact || undefined}
      className={frame}
    >
      {body}
    </div>
  )
}
code-inline.tsx
import type { ComponentPropsWithRef } from "react"
import { cn } from "@/lib/utils"

/** Plain inline code for names and short expressions within prose.
 * Use Code with compact and lang for syntax-highlighted command rows. */
export function InlineCode({
  children,
  className,
  ...props
}: ComponentPropsWithRef<"code">) {
  return (
    <code
      {...props}
      data-slot="code-inline"
      className={cn(
        "rounded-md bg-foreground/6 px-1.5 py-0.5 font-mono text-[0.875em] text-foreground/85",
        className,
      )}
    >
      {children}
    </code>
  )
}
code.css
/* The frame around highlighted code. Shiki writes both themes onto every token as
   custom properties; this picks one, so a theme switch is a repaint and never a
   re-highlight, and there is no flash of the wrong palette on first paint. */

@layer components {
  .ag-code {
    overflow: hidden;
  }
  /* The plain face's frame. The glass face is the glass's own (material-glass),
     its edge and body; a border or ground here would be a second frame. A
     selector on state, which a utility merged with the glass's could not keep
     apart. */
  .ag-code[data-material="plain"] {
    border: 1px solid var(--border);
    border-radius: 0.75rem;
    background: var(--sidebar);
  }
  .ag-code-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.375rem 0.5rem 0.375rem 0.875rem;
    border-bottom: 1px solid var(--border);
  }
  .ag-code-name {
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.75rem;
    color: var(--muted-foreground);
    user-select: none;
  }
  /* The scroller, not the <pre>: the frame keeps its corners while long lines run.
   A block is a column, its bar and then the scroller, which takes the rest of the
   frame's height: given less height than the code needs (`h-96`, a pane's
   `min-h-0 flex-1`), the code scrolls inside the frame, the frame staying whole. */
  .ag-code:not([data-compact]) {
    display: flex;
    flex-direction: column;
  }
  .ag-code:not([data-compact]) > .ag-code-scroll {
    flex: 1 1 auto;
    min-height: 0;
  }
  /* Compact commands keep the same Shiki output and theme, with the copy control
   outside the horizontal scroller so long commands never hide the action. */
  .ag-code[data-compact] {
    display: flex;
    width: fit-content;
    max-width: 100%;
    align-items: center;
    gap: 0.5rem;
  }
  /* Basis auto, not 0: the row's own width is fit-content, and a zero basis
   would tell it the command has no length, so it would wrap even with room. */
  .ag-code[data-compact] > .ag-code-scroll {
    flex: 1 1 auto;
    min-width: 0;
  }
  /* Shiki with `defaultColor: false` writes its palette as custom properties and sets
   no colour of its own, so the frame keeps its own surface with nothing to override. */
  .ag-code pre {
    margin: 0;
    padding: 1rem 0.875rem;
    background: transparent;
    font-size: 0.8125rem;
    line-height: 1.65;
    tab-size: 2;
  }
  .ag-code[data-compact] pre {
    padding-block: 0.75rem;
  }
  /* Code never breaks its lines. A line too long for the block scrolls sideways
   with the scrims' edge fade (`ag-edge-fade`, set on its scroller in code.tsx): no
   scrollbar drawn over it, the hidden side fading. */
  .ag-code code {
    display: block;
    width: fit-content;
    min-width: 100%;
    font-family: var(--font-mono, ui-monospace, monospace);
  }

  /* Shiki with `defaultColor: false` writes --shiki-light and --shiki-dark and no
   colour of its own, so the theme chooses here. */
  .ag-code span {
    color: var(--shiki-light);
  }
  .dark .ag-code span {
    color: var(--shiki-dark);
  }

  /* ---- marked lines.

   A marked line is marked EDGE TO EDGE, which is why `.line` is a block that runs the
   full scroll width: a tint that stops at the end of the text draws a ragged shape
   down the block and reads as damage rather than emphasis. The gutter bar is what
   survives being scrolled sideways, when the tint under a long line is off screen. */
  .ag-code .line {
    display: inline-block;
    width: 100%;
    padding-inline: 0.875rem;
    margin-inline: -0.875rem;
    border-left: 2px solid transparent;
  }
  .ag-code .line.highlighted {
    background: color-mix(
      in oklab,
      var(--brand, var(--foreground)) 10%,
      transparent
    );
    border-left-color: color-mix(
      in oklab,
      var(--brand, var(--foreground)) 55%,
      transparent
    );
  }
  /* An error or a warning is a highlight that has something to say, so it keeps the
   shape and changes only the hue. */
  .ag-code .line.highlighted.error {
    background: color-mix(in oklab, var(--tone-bad) 12%, transparent);
    border-left-color: var(--tone-bad);
  }
  .ag-code .line.highlighted.warning {
    background: color-mix(in oklab, var(--tone-warn) 12%, transparent);
    border-left-color: var(--tone-warn);
  }

  /* ---- a diff.

   The sign is a real character in the gutter, not a colour: colour alone is invisible
   to a reader who cannot separate red from green, and it does not survive a
   screenshot pasted into a grey chat. */
  .ag-code .line.diff {
    position: relative;
  }
  .ag-code .line.diff::before {
    position: absolute;
    left: 0.25rem;
    opacity: 0.7;
  }
  .ag-code .line.diff.add {
    background: color-mix(in oklab, var(--tone-ok) 14%, transparent);
    border-left-color: var(--tone-ok);
  }
  .ag-code .line.diff.add::before {
    content: "+";
    color: var(--tone-ok);
  }
  .ag-code .line.diff.remove {
    background: color-mix(in oklab, var(--tone-bad) 12%, transparent);
    border-left-color: var(--tone-bad);
  }
  /* Removed code is still readable, just plainly on its way out. */
  .ag-code .line.diff.remove {
    opacity: 0.65;
  }
  .ag-code .line.diff.remove::before {
    content: "-";
    color: var(--tone-bad);
  }

  /* ---- focus: everything else steps back, and comes back on hover, because a reader
   who wants the surrounding lines should not have to leave the page to see them. */
  .ag-code:has(.focused) .line:not(.focused) {
    filter: blur(2px);
    opacity: 0.5;
    transition:
      filter 200ms ease,
      opacity 200ms ease;
  }
  .ag-code:has(.focused):hover .line:not(.focused) {
    filter: none;
    opacity: 1;
  }

  /* ---- a word, marked inside the line. */
  .ag-code .highlighted-word {
    border-radius: 0.25rem;
    padding: 0.1em 0.25em;
    background: color-mix(
      in oklab,
      var(--brand, var(--foreground)) 14%,
      transparent
    );
    box-shadow: 0 0 0 1px
      color-mix(in oklab, var(--brand, var(--foreground)) 30%, transparent);
  }

  /* ---- line numbers, opt-in. A counter, so nothing is added to the copied text. */
  .ag-code-numbered code {
    counter-reset: ag-line;
  }
  .ag-code-numbered .line {
    counter-increment: ag-line;
    padding-left: 3rem;
  }
  .ag-code-numbered .line::after {
    content: counter(ag-line);
    position: absolute;
    left: 0;
    width: 2.25rem;
    text-align: right;
    color: var(--muted-foreground);
    opacity: 0.5;
    user-select: none;
  }
  .ag-code-numbered .line {
    position: relative;
  }
  .ag-code-numbered .line.diff::before {
    left: 2.5rem;
  }

  @media (prefers-reduced-motion: reduce) {
    .ag-code:has(.focused) .line:not(.focused) {
      transition: none;
    }
  }
}
code-tokens.ts
// Code's colours, character by character: the same highlighter `code` renders with
// (Shiki, VS Code's own grammars), read as one style per character instead of as
// markup, for a view that draws text which is not all there yet (an editor whose
// file is being typed: each character keeps the colour it has in the finished file,
// so a half-typed attribute already reads as one). Framework-free; Shiki loads on the
// first call, so the page ships none of it until something asks, and then only its
// core, the JavaScript regex engine and the one grammar and two themes asked for.

/** The two themes every code view here wears, light and dark, picked by the scheme
 *  it stands in. `code` and `markdown` render with them. */
export const CODE_THEMES = {
  light: "github-light",
  dark: "github-dark",
} as const

/** VS Code's default themes' token colours, which `ide` wears: Dark Modern and
 *  Light Modern are built on Dark+ and Light+. */
export const VSCODE_THEMES = {
  light: "light-plus",
  dark: "dark-plus",
} as const

/** Lines to mark, as every code view here spells them: `"2"` or `"1,4-6"`, counted
 *  from 1. Screams at anything else. */
export function linesOf(spec: string): Set<number> {
  const out = new Set<number>()
  for (const part of spec.split(",")) {
    const range = part.trim()
    if (!range) continue
    const [a, b = a] = range.split("-").map((n) => Number(n.trim()))
    if (!Number.isInteger(a) || !Number.isInteger(b) || a === undefined)
      throw new Error(`code: "${range}" is not a line or a range of lines`)
    for (let n = a; n <= (b as number); n++) out.add(n)
  }
  return out
}

/** A pair of Shiki theme ids, light and dark. */
export interface CodeThemes {
  light: string
  dark: string
}

/** One style a character can wear, in both themes. */
export interface CodeInk {
  light: string
  dark: string
  italic: boolean
  bold: boolean
}

/** A text's colours: `inks` the distinct styles, `of[i]` the one character `i` of
 *  the text wears (an index into `inks`). */
export interface CodeColours {
  inks: CodeInk[]
  of: Uint16Array
}

/** The language a path is written in: its extension (`html`, `ts`, `md`), which
 *  Shiki reads as a language or one of its aliases; `text` with none. */
export function langOf(path: string): string {
  return /\.([a-z0-9]+)$/i.exec(path)?.[1]?.toLowerCase() ?? "text"
}

type Core = import("shiki/core").HighlighterCore

/** One highlighter for the page, made on the first call: Shiki's core with its
 *  JavaScript regex engine, never the WebAssembly one (Oniguruma, 600 KB a browser
 *  would download to colour a few lines), each language and theme loaded the first
 *  time it is asked for, alone. */
let core: Promise<Core> | undefined
async function highlighter(lang: string, themes: CodeThemes): Promise<Core> {
  core ??= (async () => {
    const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] =
      await Promise.all([
        import("shiki/core"),
        import("shiki/engine/javascript"),
      ])
    return createHighlighterCore({
      engine: createJavaScriptRegexEngine(),
      themes: [],
      langs: [],
    })
  })()
  const h = await core
  const [{ bundledLanguages }, { bundledThemes }] = await Promise.all([
    import("shiki/langs"),
    import("shiki/themes"),
  ])
  const loading: Promise<void>[] = []
  if (lang !== "text" && !h.getLoadedLanguages().includes(lang)) {
    const grammar = bundledLanguages[lang as keyof typeof bundledLanguages]
    if (grammar) loading.push(h.loadLanguage(grammar))
  }
  for (const name of [themes.light, themes.dark])
    if (!h.getLoadedThemes().includes(name)) {
      const theme = bundledThemes[name as keyof typeof bundledThemes]
      if (!theme) throw new Error(`code-tokens: no Shiki theme ${name}`)
      loading.push(h.loadTheme(theme))
    }
  await Promise.all(loading)
  return h
}

/** Every character of `text` coloured by `lang`'s grammar in both `themes`. A
 *  language Shiki does not know is `text`: one ink, the theme's foreground. Runs
 *  where it is called: on the server for a still (passed to a view as its
 *  `colours`), or in the browser for text that changes. */
export async function codeColours(
  text: string,
  lang: string,
  themes: CodeThemes = CODE_THEMES,
): Promise<CodeColours> {
  const h = await highlighter(lang, themes)
  const lines = h.codeToTokensWithThemes(text, {
    lang: h.getLoadedLanguages().includes(lang) ? lang : "text",
    themes: { light: themes.light, dark: themes.dark },
  })
  const inks: CodeInk[] = []
  const known = new Map<string, number>()
  const of = new Uint16Array(text.length)
  for (const line of lines)
    for (const token of line) {
      const light = token.variants.light
      const dark = token.variants.dark
      if (!light || !dark)
        throw new Error("code-tokens: a token without both themes' colours")
      // Shiki's FontStyle flags: 1 italic, 2 bold.
      const style = light.fontStyle ?? 0
      const ink: CodeInk = {
        light: light.color ?? "inherit",
        dark: dark.color ?? "inherit",
        italic: (style & 1) !== 0,
        bold: (style & 2) !== 0,
      }
      const key = `${ink.light} ${ink.dark} ${style}`
      let n = known.get(key)
      if (n === undefined) {
        n = inks.length
        inks.push(ink)
        known.set(key, n)
      }
      of.fill(n, token.offset, token.offset + token.content.length)
    }
  return { inks, of }
}

/** A stretch of a line in one ink (null: the view's own foreground, before the
 *  colours have loaded). */
export interface CodeRun {
  text: string
  ink: CodeInk | null
}

/** `text` cut into lines of runs, each character inked by `colours` at its offset
 *  in the text they were read from: `origin[i]` for character `i` (a file being
 *  typed, coloured as it will be), or `i` itself without one. */
export function runsOf(
  text: string,
  colours: CodeColours | null,
  origin?: readonly number[],
): CodeRun[][] {
  const lines: CodeRun[][] = [[]]
  let line = lines[0] as CodeRun[]
  let run: CodeRun | null = null
  for (let i = 0; i < text.length; i++) {
    const ch = text[i] as string
    if (ch === "\n") {
      line = []
      lines.push(line)
      run = null
      continue
    }
    const at = origin ? (origin[i] as number) : i
    const ink = colours
      ? (colours.inks[colours.of[at] as number] ?? null)
      : null
    if (run && run.ink === ink) run.text += ch
    else {
      run = { text: ch, ink }
      line.push(run)
    }
  }
  return lines
}
use-code-colours.ts
"use client"

import * as React from "react"
import {
  CODE_THEMES,
  type CodeColours,
  type CodeThemes,
  codeColours,
} from "@/registry/base-nova/lib/code-tokens"

/** `text`'s colours in `lang` (lib/code-tokens), read in the browser once it
 *  renders: null until Shiki has loaded, so a view draws the text plain at once and
 *  coloured a moment later, never blank. `given`, colours already read (on the
 *  server, for a still), are used as they are, and the browser loads no Shiki. */
export function useCodeColours(
  text: string,
  lang: string,
  themes: CodeThemes = CODE_THEMES,
  given?: CodeColours,
): CodeColours | null {
  const [colours, setColours] = React.useState<{
    key: string
    colours: CodeColours
  } | null>(null)
  const { light, dark } = themes
  const key = `${light} ${dark} ${lang}\n${text}`
  React.useEffect(() => {
    if (given) return
    let live = true
    void codeColours(text, lang, { light, dark }).then((c) => {
      if (live) setColours({ key, colours: c })
    })
    return () => {
      live = false
    }
  }, [given, key, text, lang, light, dark])
  if (given) return given
  return colours?.key === key ? colours.colours : null
}