<!-- GENERATED by scripts/build-llms.mjs from llms/conversation.md — do not edit this file. -->

# `lr-code-block`

- **Import** `import '@aceshooting/lyra-ui/components/lr-code-block.js';` (stable tag alias; registers the tag)
- **Class** `LyraCodeBlock`, also available unregistered from `@aceshooting/lyra-ui/components/conversation/code-block/code-block.class.js`
- **Family** `components/conversation/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** `shiki` — see `llms/peers.md`
- **Themeable via** 15 parts, 9 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-code-block`

Fenced code display with optional lazy syntax highlighting and a copy button. First-party invention
(no Web Awesome equivalent). It lazily loads the optional `shiki` peer for syntax highlighting and
includes a compact GreyCat/GCL grammar because
Shiki does not bundle one. It falls back to a plain `<pre><code>` when that peer isn't installed or
`language` is unset/unrecognized. That
fallback is the _default_ rendering path, not a degraded one: unhighlighted code is perfectly usable,
and it's what every instance renders at zero extra bytes until shiki resolves.

Removing `code`, `language`, or `highlight-lines` treats that input as absent: source becomes empty,
an absent language selects plain text, and attribute-based emphasis clears. Removal preserves Lit's
`null` property readback; an explicitly empty attribute stays an empty string, and a later value
resumes normal rendering. Highlighted gutter labels and locale-formatted line numbers follow live
`.strings` and inherited or explicit locale changes. Unrelated updates retain the existing
highlighted markup.

**Properties:**

- `code: string = ''` — the raw source text
- `language: string = ''` — a shiki-recognized language id or alias (e.g. `"javascript"`, `"python"`,
  `"json"`); when unset, or when shiki doesn't recognize it, the code renders as plain unhighlighted
  text regardless of whether shiki itself is available. Shiki's bundled catalog covers most common
  programming, markup, data, and configuration languages, including Python, C, C++, C#, Java,
  JavaScript, TypeScript, HTML, CSS, JSON, SQL, Go, Rust, YAML, Markdown, and shell scripts. Lyra
  also includes a built-in GreyCat grammar; use `"gcl"` or `"greycat"` for GreyCat source.
- `filename: string = ''` — shown in the header, when set
- `accessibleLabel: string | null = null` (attribute `aria-label`) — names the internal focusable code-body
  region; otherwise a localized filename/language description is generated
- `collapsible: boolean = false` (reflected) — shows the collapse/expand chevron button
- `collapsed: boolean = false` (reflected) — only has a visible effect while `collapsible` is also
  true
- `copyable: boolean = true` (reflected) — shows the copy-to-clipboard button. Literal HTML
  `copyable="false"` disables it; use a property binding such as `.copyable=${false}` when binding a
  value. A `?copyable=${false}` boolean-attribute binding only removes the attribute and cannot
  override the true default.
- `maxHeight: string = ''` (attribute `max-height`) — a CSS length (e.g. `"20rem"`); once set, the
  code scrolls internally past this height instead of growing the page
- `lineNumbers: boolean = false` (attribute `line-numbers`, reflected) — displays one-based line
  numbers for both highlighted output and the plain-text fallback
- `highlightLines: string = ''` (attribute `highlight-lines`) — comma-separated 1-based inclusive
  line ranges (e.g. `"3-5,7"`) to visually emphasize. Declarative sugar over `highlights` — merges
  with, and renders identically to, any `line-range` entries there. Malformed segments are ignored
  while valid segments still apply.
- `activatableLines: boolean = false` (attribute `activatable-lines`) — turns the
  (`lineNumbers`-gated) gutter into a roving-tabindex group of buttons emitting `lr-line-activate`.
  Has no effect while `lineNumbers` is unset. If controlled `code` shrinks while a line owns
  focus, focus follows the clamped surviving line; moving focus elsewhere during that update wins.
- `highlights: readonly LyraHighlight[] = []` (attribute: false) — host-supplied highlights to paint over the
  code (the shared anchor-target `LyraHighlight` contract from `document-viewer/anchors.ts`). Only
  `line-range` anchors are meaningful here — every other `LyraAnchor` kind is ignored.
- `activeHighlightId: string | null = null` (attribute `active-highlight-id`) — the `highlights`
  entry, if any, currently treated as active (`data-active` on its lines).
- `anchorKinds: LyraAnchor['kind'][] = ['line-range']` — readonly, for the shared anchor-target
  contract.
- `languages?: Record<string, ShikiLanguageInput>` (attribute: false) — a map of language id to an
  already-imported shiki grammar module (e.g. `{ bash: bashGrammar }` from a module-scope
  `import bash from 'shiki/langs/bash.mjs'`). When `language` matches a key here, highlighting is
  seeded from exactly that pre-supplied grammar via a fine-grained `createHighlighterCore()`
  highlighter, bypassing the default ~200-language dynamic-import path (`loadShikiHighlighter()`)
  for that language — an additive, opt-in escape hatch for a build scoped to just the languages a
  consumer actually needs. A `language` absent from this map (or `languages` left unset) falls back
  to the default dynamic-import path unchanged. For a TypeScript annotation, use
  `import type { ShikiLanguageInput } from '@aceshooting/lyra-ui/components/conversation/code-block/code-block.js'`;
  the type-only granular import emits no registration side effect.
  The map key need not be the grammar module's own registered name or one of its declared
  aliases — Lyra derives the Shiki `langAlias` mapping this needs automatically, so a key such as
  `tsx` reusing a differently-named grammar (e.g. TypeScript's own module, to avoid bundling a
  second near-identical grammar) still highlights under that key.

- `copyAppearance: 'text' | 'icon' = 'text'` (attribute `copy-appearance`, reflected) — how the
  header's copy control presents itself. `'text'` is the labelled button this component has always
  rendered; `'icon'` swaps the visible label for a compact glyph and promotes the same localized
  Copy/Copied/failure string to the control's accessible name

**Methods:** `scrollToAnchor(target)` — resolves a `line-range` anchor (or a `highlights` id string
resolving to one) by scrolling its start line into view within `[part="body"]`; resolves `false`
when the anchor isn't a `line-range`, the id isn't found, or the start line is out of bounds.
`refreshTheme(): void` re-reads the resolved theme for syntax highlighting.

**Events:** `lr-copy` (frozen `detail: { ok: true, text }` — fires only after the raw `code` value
was written successfully), `lr-error` (`detail: null` — generic notification when clipboard writing
fails), `lr-copy-error` (frozen `detail: { ok: false, text, reason, error }`, where `reason` is
`'unsupported' | 'denied' | 'failed'`), `lr-toggle-request` (cancelable;
`detail: { collapsed }` is the proposed next state and canceling leaves `collapsed` unchanged),
`lr-toggle` (`detail: { collapsed: boolean }` — the committed state after the request is accepted),
`lr-line-activate` (`detail: { line: number }` — a gutter line number was activated while
`activatableLines` is set),
`lr-text-select` (`detail: { text, anchor, rects }` — a text selection inside the code body ended;
`anchor` is a `line-range` anchor covering the selected lines)

**Slots:** `header-actions` — extra controls for the header row, rendered after the copy control.
Their presence alone is enough to render the header, and appending or removing such a child at any
time brings the header into existence or retires it (the host's own light DOM is observed; no
property write is needed).

**CSS parts:** `base`, `header`, `filename`, `language`,
`copy-button` (the copy control, a composed `<lr-icon-button>` as of 16.0.0; it also carries
`copy-button-text` or `copy-button-icon` for the active `copyAppearance`, so match it by token —
`[part~="copy-button"]` — not by exact value), `copy-button-text` / `copy-button-icon` (the copy
control in each appearance), `copy-button__control` (the copy control's own native `<button>`),
`header-actions` (the wrapper around the `header-actions` slot; it carries the `hidden` attribute
and computes to `display: none` whenever nothing is assigned, so an empty slot contributes no
header gap — a rule that sets `display` on it must qualify itself with `:not([hidden])`),
`toggle`, `body`, `pre`,
`code`, `line-highlight` (a line marked by `highlightLines` or a `line-range` entry in `highlights`),
`line-button` (a gutter line-number button, only rendered while `activatableLines` and `lineNumbers`
are both set)

**Migrating a pre-16.0.0 `::part()` rule.** This component's icon-only action is a composed
`<lr-icon-button>`, so the part naming that action now names the composed child's HOST, which
paints nothing. A `border`, `background` or `border-radius` set on it is silently dead — only
`color` still appears to work, because it inherits, which makes such a rule look half-alive rather
than broken. Set `--lr-icon-button-background`/`-color`/`-border`/`-radius` (and their
`-hover`/`-active` variants) on this element or an ancestor instead: the composed control reads
those public tokens ahead of any default this component supplies. For SIZE use
`--lr-theme-icon-button-size`, not `--lr-icon-button-size` — every `LyraElement` re-declares the
latter on its own `:host`, so it never reaches a composed child (see `llms/tokens.md`).

**Border reaches the composed copy control the same way background/color/radius do.** This
component paints no resting border of its own, so it relays no `--_lr-icon-button-border-default`
into the copy control's private fallback tier — but that absence is not a gap. The public
`--lr-icon-button-border` (and its `-hover`/`-active` variants) is the FIRST arm of the token
chain, resolved by ordinary custom-property inheritance regardless of whether this component
relays a default for that same property, so setting it on this element or an ancestor reaches the
copy control exactly as the background/color/radius tokens do. A component with no resting
border simply has no default to relay, which is different from border theming being broken. Size
remains the one exception that does not cross this way: use `--lr-theme-icon-button-size`, never
`--lr-icon-button-size`, as noted above.

**Themeable custom properties:** `--lr-code-block-max-height` (default `none` — an independently
settable scroll cap; a `max-height` attribute writes the same property inline on `body` and wins),
`--lr-code-block-font` (default
`var(--lr-font-mono)`, the library's shared monospace stack), `--lr-code-block-tab-size` (default `2` — tab width for the
rendered code, applied to `[part='pre']`), `--lr-code-block-active-line-outline-color` (default
`var(--lr-color-brand)` — the outline around the line marked active by `active-highlight-id`),
`--lr-code-block-highlighted-line-bg` (default `var(--lr-color-warning-quiet)` — the background of a
line marked by `highlight-lines` or a `line-range` entry in `highlights`, in both the light and
dark-theme rendering paths), `--lr-code-block-language-bg` (default `var(--lr-color-brand-quiet)`)
and `--lr-code-block-language-color` (default `var(--lr-color-brand)`) — the header `language` pill's
background and text color, independent of the active-line outline above — plus shared tokens
`--lr-color-border`, `--lr-radius`,
`--lr-color-surface`, `--lr-space-xs/-s/-m`, `--lr-font`, `--lr-color-text-quiet`,
`--lr-color-text`, `--lr-color-brand`/`-brand-quiet`, `--lr-transition-fast`,
`--lr-focus-ring-width/-color/-offset`. `body`, the scroll container, also honors the opt-in
theme-level `--lr-theme-scrollbar-width`/`--lr-theme-scrollbar-gutter` hooks (defaults `auto`/`auto`,
matching its previous unconditional `scrollbar-width: auto`) — set either on `:root` or any
ancestor for one declaration to retheme every internal scroll container in the library, including
`lr-table`, `lr-virtual-list`, `lr-scroller`, `lr-carousel`, and `lr-code-editor`.

`base` is a flex column and `body` grows to fill whatever block space a definite-height host
gives it, still capped by `--lr-code-block-max-height` and still independently scrollable. An
unsized host is unaffected — this is a fill capability, not a change to the default
content-sized rendering.

`--lr-code-block-tab-size` carries the same default as `--lr-code-editor-tab-size`, so the editable
and read-only code surfaces agree on what a literal tab looks like. It is declared as a `var()`
fallback **at the point of use, not on `:host`** — a `:host` rule is re-stamped on every instance and
shadows any inherited value, so a page- or container-level declaration could never reach it. It is
also never written as an inline `tab-size`: `shiki` puts its own `style` attribute on the highlighted
`<pre>`, and an inline declaration is the one thing a host override cannot beat. `<lr-markdown>` and
`<lr-markdown-core>` carry the same fallback for their own `code-block` part because they are
**sibling** custom elements rather than descendants of this one — no single declaration covers both.
The identical value can still look different across the two: this component is `white-space: pre`
while a markdown code block inherits `pre-wrap`, and tab stops restart at each visual line, so a
wrapped line's tabs diverge.

`--lr-code-block-active-line-outline-color` retints just the active line's outline and leaves every
other `--lr-color-brand` surface in the component — hover states, the focus
ring — alone. It too is an inline `var()` fallback rather than a `:host` declaration, deliberately,
so it inherits: set it on the element, on an ancestor, or at the theme level.
`--lr-code-block-language-bg`/`--lr-code-block-language-color` retint the header language pill on
their own, independent of both the active-line outline and the hover/focus states.

`--lr-code-block-highlighted-line-bg` follows the same pattern: an inline `var()` fallback (not a
`:host` declaration) so it inherits, retinting just the highlighted-line background and leaving every
other `--lr-color-warning-quiet` surface alone.

**Optional peer deps:** `shiki` — requested only when syntax highlighting is used and shared by
code blocks on a page. Languages load on demand. If the peer or requested language is unavailable,
every instance retains a readable plain-text fallback; install it with `pnpm add shiki` to enable
highlighting.

```ts
import { html } from "lit";
import "@aceshooting/lyra-ui/components/conversation/code-block/code-block.js";

const view = html`<lr-code-block
  language="typescript"
  filename="sum.ts"
  collapsible
  max-height="20rem"
  .code=${`export function sum(a: number, b: number) {\n  return a + b;\n}`}
  @lr-copy=${(e) => console.log("copied", e.detail.text)}
></lr-code-block>`;
```

Set `line-numbers` when source context benefits from numbered lines. The option does not change the
raw `code` value or the `lr-copy` event payload.

A decorative `<lr-skeleton shape="rect">` placeholder (with its own announcements disabled and
`aria-busy="true"` on the host) stands in only while shiki itself is loading for the very first time
on the page and `language` is set — it is
deliberately _not_ shown again for a later per-language grammar fetch (that's typically fast, and the
plain-text fallback already reads fine as a placeholder for it). Internally, a shiki `transformer`
(`partTransformer`) rewrites shiki's generated `<pre>`/`<code>` nodes in a single pass to carry this
component's own `part="pre"`/`part="code"` hooks and strips shiki's default `tabindex="0"` from
`<pre>`, since `[part="body"]` is already the single scrollable/focusable region (`role="group"`,
`tabindex="0"`) for the code area. Dark mode is handled via shiki's own "dual themes" feature: every
token carries its light color as a plain inline `color`/`background-color` and its dark color in
`--shiki-dark`/`--shiki-dark-bg` custom properties. The component watches its resolved Lyra theme
tokens and sets `data-dark-theme="true"` on the code body when the effective surface is dark; an
`!important` state rule then activates Shiki's dark values. This follows explicit Lyra theme
overrides instead of consulting `prefers-color-scheme` directly. Shiki's generated colors are the
one deliberate exception to every other color being a `--lr-*` token.

**Known gotchas:**

- `copyable` defaults to `true` and reflects — literal `copyable="false"` and a `.copyable=${false}`
  property binding both disable it; a `?copyable=${false}` boolean-attribute binding does not.
- if `code` or `language` changes while highlighting is loading, only the result matching the
  current values is rendered.
- a malformed `code`/`language` combination that makes shiki's `codeToHtml()` throw falls back to
  plain text silently, not a blank code block.
- the "Copied!" label appears only after clipboard fulfillment and reverts to "Copy" after 1500ms.
  Rejection or an unavailable clipboard instead shows the localized failure state and emits
  `lr-error` plus `lr-copy-error`; the helper resolves the clipboard from the element's current
  `ownerDocument`, including after adoption.

---
