Skip to content

CodeBlock, with variants; and a pop-up list as wide as its widest option - #46

Merged
jasperblues merged 5 commits into
mainfrom
feat/code-block
Oct 29, 2026
Merged

jasperblues merged 5 commits into
mainfrom
feat/code-block

Conversation

@jasperblues

@jasperblues jasperblues commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

A CodeBlock for code people copy, such as a snippet to paste into a program or a command to run.

<CodeBlock code={string} language?={'python'|'typescript'|'javascript'|'kotlin'|'yaml'|'shell'|'json'|'text'}
           label?={string} copy?={true} highlight?={(code, language) => ReactNode} className? />
<CodeBlock variants={{ value, label, language, code }[]} value?={string} onValueChange?={(value) => void}
           label? copy? highlight? className? />
  • The block. A framed <pre><code> on the page's ground (--desk-page, with the accent tint). It is monospace, and its lines keep their length, so a long one scrolls sideways. The block is a named region that takes focus, so it scrolls from the keyboard. Its name is label, which is also shown above the code, or failing that "Python code", "YAML code" and so on, or "Code" for text. One trailing newline is not drawn, but it is still copied.
  • Highlighting, with no dependency. A small scanner per language marks comments, strings, keywords, numbers and the keys of YAML and JSON. Each is a span with a class (desk-code-comment, -string, -keyword, -number, -key), and the spans are built as React nodes, so code is always text and never HTML. The rules that keep it honest:
    • # starts a comment only at the start of a word in shell and YAML.
    • YAML opens a quote only where a value starts.
    • Python's triple-quoted strings and JS/TS template strings run over lines, and Kotlin's raw strings do too.
    • A block comment left open, as in a regular expression, colours only its own line.
  • New tokens. --desk-code-comment|string|keyword|number|key are set for light, system dark and chosen dark. A test reads tokens.css and checks each one at 4.5:1 against the page ground, tinted and untinted. Looks never change colour tokens, so every look is covered.
  • highlight. A host's own highlighter, used instead of the built-in one.
  • Copy. A quiet desk Button in a column of its own at the top right, so it never sits over the code. It says Copy, then Copied for 2 s, and a role="status" region announces it. When there is no clipboard (plain http, or the browser refuses), it selects the code and tries execCommand('copy'). If that fails too, the button says "Press ⌘C" or "Press Ctrl+C" and the code stays selected. copy defaults to true.
  • Variants. The same code in several forms, such as an install with pip or uv, or a client in two languages. With variants, the block gets a bar across the top of its frame: the tabs on the left and Copy on the right, so the block reads as one card, as on iOS, rather than a control above a box.
    • The tabs are desk's small SegmentedControl sitting in the bar, so they are one tab stop and the arrow keys move between them.
    • Copy copies the variant shown.
    • The choice is uncontrolled by default; value and onValueChange let the host hold it.
    • label names the tabs and the region, and is not drawn.
    • The props are a union, so a block takes code and language or variants, never both.
  • Exports. CodeBlock, CodeBlockProps, CodeLanguage and CodeVariant, from @liberation-data/desk/react.
  • Docs. llms.txt (in the controls list, plus the tokens under Styling), the README (under Controls), docs/guide.md (in the Choosing a component table), and a Code subsection in the HIG under §6 Controls on when to use a code block rather than a read-only TextField or inline <code>.
  • Demo. The garage sample gets two blocks:
    • About has a "Run it yourself" block with npm and pnpm variants.
    • Settings has "As a file", a live YAML view of what the window is set to.
  • Size. The react budget goes from 62 to 68 KiB. CodeBlock with its variants is 4.6 KiB gzipped, and react had 0.9 KiB to spare; the note in scripts/size.mjs says why.

A fix in the same release: the pop-up list is as wide as its widest option

.desk-popup-list was positioned inside an anchor only as wide as its button. So a short button squeezed the menu to its own width and wrapped each label, checkmark and all ("Your world" broke over two lines). The fix is its own commit:

  • The list is now width: max-content, with min-width: 100% and max-width: min(360px, calc(100vw - 32px)).
  • An option's label stays on one line (white-space: nowrap), and its description may wrap within that maximum.
  • When the wider list would cross its window's right edge, it moves left until it fits, but never past the window's left edge (8 px margin, the same way Popover clamps).
  • Tests in collections.test.tsx read the stylesheet for the width rules and the one-line label, and check the left shift: it moves to fit, stops at the left edge, and doesn't move when it fits.
  • Checked in Chrome: a 90 px button over "Kilometres and metres" now gives a 180 px list, with each option on one line.

Tests

test/react/codeBlock.test.tsx has 36 tests:

  • Tokens for each language:
    • A # inside a Python string is not a comment.
    • Escaped quotes don't end a string.
    • A Python triple-quoted string runs over lines.
    • YAML keys are found, including quoted and list-item keys, and Don't and http://x/#top stay text.
    • JSON keys are told apart from string values.
    • In shell, $# is not a comment, --if-exists is not a keyword, and a heredoc apostrophe doesn't open a string.
    • Kotlin raw strings work.
    • An unclosed /* colours only its own line.
    • A long YAML line still takes time in line with its length.
  • Escaping: <script> and <img onerror> stay text.
  • The highlight prop replaces the built-in highlighting.
  • Keyboard: Tab reaches each region, then its Copy button.
  • Copy:
    • Copied, its announcement, and the reset after 2 s.
    • The execCommand fallback, and the select-and-press-keys fallback.
    • A change of code resets Copied.
    • A copy that finishes after unmount, under StrictMode, leaves no timer.
  • The contrast of every code token.
  • Variants:
    • Switching changes the code shown, its language and what Copy copies.
    • Arrow keys move between variants, and Tab goes tabs, then Copy, then the region.
    • The bar holds the tabs and Copy inside the frame.
    • A controlled value is followed.

npm run check passes: typecheck, 61 files and 632 tests, the build, and size (react 65.8 of 68, css 19.7 of 20).

…long YAML lines stay linear, and Copy never outlives its code or its block
@jasperblues jasperblues added the release: minor A feature: the next release is a minor (the default when src/ changes) label Oct 29, 2026
@jasperblues jasperblues changed the title CodeBlock: code people copy, highlighted for its language, with Copy in its corner CodeBlock, with variants; and a pop-up list as wide as its widest option Oct 29, 2026
@jasperblues
jasperblues merged commit 8b8dc43 into main Oct 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release: minor A feature: the next release is a minor (the default when src/ changes)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant