Code Block
A read-only panel for a request body, a response, an event’s JSON, an
install command. Mono text on the code surface, colored by
Shiki with the grammar for its language, a title
row with expand, wrap, and copy controls. Past maxLines the body scrolls
inside that height; overflow="expand" keeps the old footer fold. Wide
lines scroll sideways inside the panel; the page never does.
Import
import { CodeBlock } from "@photon-ai/pho-ui/components/code-block";
Languages
language names what the text is. Twenty-seven grammars — json,
typescript, javascript, tsx, bash, python, swift, go,
ruby, php, java, kotlin, csharp, rust, html, css, xml,
yaml, toml, dotenv, sql, graphql, http, markdown, diff,
jsonc, jsx — and the short names people reach for (ts, js, sh,
py, yml, md). text, the default, colors nothing. A name the panel
does not know reads as text.
Each grammar is fetched the first time a panel asks for it, and Shiki
itself with the first grammar — a page of cards and rows never loads the
engine. The body reads plain for the frame that takes, then the colors
settle in; a grammar already here colors in the first render. Call
preloadLanguage("ts") where you know a panel is coming.
import { Photon } from "@photon-ai/sdk";const photon = new Photon({ apiKey: process.env.PHOTON_API_KEY });// Send a text and wait for the delivery receipt.export async function notify(to: string, text: string): Promise<Receipt> { const message = await photon.messages.send({ to, text, platform: "sms" }); return photon.receipts.wait(message.id, { timeout: 30_000 });}<CodeBlock title="notify.ts" language="ts" code={source} />
<CodeBlock title="Event body" language="json" code={event} />
<CodeBlock language="bash" code="npm install @photon-ai/sdk" />
JSON
JSON has a synchronous face: a built-in tokenizer colors keys, strings, numbers, and the three keywords in the first render — on the server too — and the grammar, when it arrives, agrees with it token for token, so a body never flashes plain. A body that isn’t a string (an object) is pretty-printed with two-space indent.
Snippets
Without a title there is no title row. The controls float over the
panel’s top-right corner, on the code surface, the way a command on a
page wears its copy button.
npm install @photon-ai/sdkcurl -X POST https://api.photon.codes/v1/messages \ -H "Authorization: Bearer $PHOTON_API_KEY" \ -H "Content-Type: application/json" \ -d '{"to": "+15403908722", "text": "On its way."}'Colors
Every scope a grammar knows routes to one of eight kinds, and each kind
is one --color-pho-code-* token, which carries light and dark: key,
string, number, keyword, punctuation, comment, function,
type. Operators, variables, and everything else stay in the panel’s ink
(fg, on bg). A body reads as prose with a few things picked out, not
as confetti. The theme lives in the package; nothing to configure, no
Shiki theme to pick.
Wrapping
Long lines scroll sideways by default, so a body reads the way it was
sent. The toggle wraps them instead; defaultWrap starts wrapped, and
wrap with onWrapChange owns it. wrapToggle={false} hides the
control; the copy button stays.
POST /v1/messages HTTP/1.1Host: api.photon.codesAuthorization: Bearer pho_sk_live_51Kk9fD3aB7cD2eF4gH6iJ8kL0mN2oP4qR6sT8uV0wX2yZ4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZIdempotency-Key: beab9fa1-f9e4-4908-b27a-7971ddc39c20Content-Type: application/jsonPOST /v1/messages HTTP/1.1Host: api.photon.codesAuthorization: Bearer pho_sk_live_51Kk9fD3aB7cD2eF4gH6iJ8kL0mN2oP4qR6sT8uV0wX2yZ4aB6cD8eF0gH2iJ4kL6mN8oP0qR2sT4uV6wX8yZIdempotency-Key: beab9fa1-f9e4-4908-b27a-7971ddc39c20Content-Type: application/json<CodeBlock title="Request headers" code={headers} wrapToggle={false} />
Overflow
Past maxLines (30 by default) the body is clipped to that many lines and
scrolls inside the panel. The title row’s expand toggle — same 32px
squircle as wrap — reads Show all 58 lines; pressed, the panel grows to
the full text and the label becomes Collapse to 24 lines. Copy still
writes every line. The inner scroll hard-clips at the border; the
ScrollArea edge fade stays off.
overflow="expand" keeps the footer button, Show all 58 lines, with a
chevron that flips when the panel opens. Expanded, the button reads
Show less. Use it when a reader should unfold rather than scroll.
{ "id": "pho_evt_615jeffmnmgpksp7t54c0jkwmj", "apiVersion": "2026-07-01", "type": "message.delivered", "source": "sms.photon", "timestamp": "2026-09-08T01:43:09.469Z", "data": { "object": { "addressedTo": { "details": { "resourceId": "pho_res_01m04axjqrfzxv4xjet40a84gb" }, "handle": "+15403908722", "id": "pho_usr_3np2kr3q2ah8rb13crb4v3a8ap", "kind": "agent", "metadata": {}, "platform": "sms", "projectId": "pho_prj_01kztbv929fagsn0pk422y682f" }, "content": { "format": "plain", "metadata": { "photon": { "adaptations": []<CodeBlock title="Event body" language="json" code={event} maxLines={24} />
<CodeBlock
title="Event body"
language="json"
code={event}
maxLines={24}
overflow="expand"
/>
expandToggle={false} hides the header toggle and leaves the clipped
scroll. The header order is expand, wrap, copy; each can be hidden.
Line numbers
lineNumbers draws a gutter. Off by default; turn it on for a long
payload, about twenty lines or more. Numbers are not selectable, so a
copy still writes only the code. A wrapped line keeps one number. The
gutter sticks to the left when a long line scrolls sideways.
<CodeBlock
title="Event body"
language="json"
code={event}
maxLines={24}
lineNumbers
/>
Empty
With no code the panel keeps its title and says empty in its place,
Nothing to show. unless told otherwise. No copy, no wrap.
This request sent no body.
On a page
BasicPage.Code is this panel under the page’s name, placed in the stack
as a block with its own rhythm. See Basic Page.
Props
- code —
string | object | null— the text; an object is printed as JSON - language — a grammar or alias, or
text(defaulttext) - title —
ReactNode— the title row’s words; without one the controls float - maxLines —
number(default30) — the clip / fold height - overflow —
scroll/expand(defaultscroll) — clip and scroll, or fold with a footer - wrap / defaultWrap / onWrapChange — wrapping, controlled or not
- expandToggle —
boolean(defaulttrue) — the header expand control (scrollonly) - wrapToggle —
boolean(defaulttrue) — the wrap control in the title row - lineNumbers —
boolean(defaultfalse) — a gutter of line numbers - copy —
boolean(defaulttrue) — the copy control - empty —
ReactNode(defaultNothing to show.) - actions —
ReactNode— more controls beside the built-in ones, before them - …plus any
<div>attribute
Also exported, for a host that wants the tokens without the panel:
highlight(code, language) resolves to lines of { type, text },
highlightSync does the same for a grammar already here, useHighlight
is the hook the panel uses, preloadLanguage fetches a grammar early,
CODE_TOKEN_CLASS maps a kind to its class, and tokenizeJson(line) is
the synchronous JSON face.
Accessibility
- Text stays text. The body is a
<pre><code>: selectable, searchable, read line by line. Colors are only how it looks. - Controls are named.
Wrap long linesis a pressed / unpressed toggle;Copy request body(the title, lowercased;Copy codewithout one) announcesCopied; the expand toggle isShow all N lines/Collapse to N lineswitharia-pressed; the footer fold (overflow="expand") carriesaria-expanded. - The scroll region is a region. When the body is clipped it is focusable, named after the title, and keeps a visible focus ring.
Best practices
- Name the panel for what it holds:
Request body,Response,Event body,notify.ts. Leave a one-line command untitled. - Say the language.
textis for a body whose shape you don’t know. - Pass the object where you have one; the panel prints it. Pass the raw text where the shape matters (a body as sent).
- Keep
maxLineswhere a reader can still see there is more, twenty to forty. The default scroll keeps the page still;overflow="expand"is for a reader who should unfold. - Turn on
lineNumbersfor a long payload (about twenty lines or more).