feat(markdown): copy button on fenced code blocks (LC-329) #341

Merged
longjacksonle merged 1 commit from feat/lc-329-code-block-copy into main 2026-06-17 04:05:31 +02:00

What

A hover "Copy" button on each fenced code block in a rendered message, copying the block's raw text with a brief checkmark + "Copied" confirmation. Implements LC-329 / LC-330.

How

  • Render (markdown.rs): highlight_code wraps the syntect <pre> (and the plain-text fallback) in <div class="lc-codeblock"> with an icon-only <button class="lc-code-copy" data-lc-code-copy> (inline clipboard SVG). Both code-block branches get the wrapper.
  • Cache-safe i18n: the markdown render is cached keyed on (body, mentions, emojis), NOT locale (the LC-290 spoiler comment is explicit). So the button carries no localized text. A delegated handler in layout.html (locale-rendered per request, where the other global handlers live) applies the accessible name/tooltip from the code-copy/code-copied strings and handles the click - copying the sibling <pre>.textContent via navigator.clipboard with the __lcCopyFallback fallback, then flashing a checkmark + "Copied" for ~1.5s. It re-labels on htmx:afterSettle/htmx:oobAfterSwap, so blocks in live-arriving/edited messages and on the wiki / search surfaces all work.
  • CSS (main.css): .lc-codeblock relative; .lc-code-copy absolute top-right, revealed on hover / keyboard focus; a .lc-code-copied state swaps the icon for a \2713 checkmark. Uses the existing theme tokens (--surface-elevated, --border, --content).
  • i18n: reused the pre-existing code-copy / code-copied keys (en "Copy"/"Copied", es "Copiar"/"Copiado") rather than adding duplicates.

Testing

  • just check (clippy standalone + saas, fmt) clean.
  • just test and just test-saas green. New views::markdown unit tests: a fenced block (highlighted and unknown-language fallback) gets lc-codeblock + data-lc-code-copy and keeps <pre>, with no localized text baked in; a plain message gets neither. (An initial pass duplicated the code-copy Fluent keys and tripped the i18n parse tests; fixed by reusing the existing keys.)

Operator impact

None. No new env var, config, or contract change; no [operator-action] marker.

🤖 Generated with Claude Code

## What A hover "Copy" button on each fenced code block in a rendered message, copying the block's raw text with a brief checkmark + "Copied" confirmation. Implements LC-329 / LC-330. ## How - **Render** (`markdown.rs`): `highlight_code` wraps the syntect `<pre>` (and the plain-text fallback) in `<div class="lc-codeblock">` with an **icon-only** `<button class="lc-code-copy" data-lc-code-copy>` (inline clipboard SVG). Both code-block branches get the wrapper. - **Cache-safe i18n**: the markdown render is cached keyed on `(body, mentions, emojis)`, NOT locale (the LC-290 spoiler comment is explicit). So the button carries no localized text. A delegated handler in `layout.html` (locale-rendered per request, where the other global handlers live) applies the accessible name/tooltip from the `code-copy`/`code-copied` strings and handles the click - copying the sibling `<pre>.textContent` via `navigator.clipboard` with the `__lcCopyFallback` fallback, then flashing a checkmark + "Copied" for ~1.5s. It re-labels on `htmx:afterSettle`/`htmx:oobAfterSwap`, so blocks in live-arriving/edited messages and on the wiki / search surfaces all work. - **CSS** (`main.css`): `.lc-codeblock` relative; `.lc-code-copy` absolute top-right, revealed on hover / keyboard focus; a `.lc-code-copied` state swaps the icon for a `\2713` checkmark. Uses the existing theme tokens (`--surface-elevated`, `--border`, `--content`). - **i18n**: reused the pre-existing `code-copy` / `code-copied` keys (en "Copy"/"Copied", es "Copiar"/"Copiado") rather than adding duplicates. ## Testing - `just check` (clippy standalone + saas, fmt) clean. - `just test` and `just test-saas` green. New `views::markdown` unit tests: a fenced block (highlighted and unknown-language fallback) gets `lc-codeblock` + `data-lc-code-copy` and keeps `<pre>`, with no localized text baked in; a plain message gets neither. (An initial pass duplicated the `code-copy` Fluent keys and tripped the i18n parse tests; fixed by reusing the existing keys.) ## Operator impact None. No new env var, config, or contract change; no `[operator-action]` marker. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(markdown): copy button on fenced code blocks (LC-329)
All checks were successful
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Kingfisher (push) Successful in 9s
check-secrets / TruffleHog (pull_request) Successful in 8s
check-secrets / Kingfisher (pull_request) Successful in 8s
check-secrets / Nosey parker (pull_request) Successful in 9s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 9m36s
98f9f99cb4
Add a hover Copy button to each fenced code block in a rendered message. `highlight_code` wraps the syntect `<pre>` (and the plain-text fallback) in `<div class="lc-codeblock">` with an icon-only `<button data-lc-code-copy>`. The button carries NO localized text, so the content-keyed markdown cache (keyed on body/mentions/emojis, not locale) stays correct across locales - a delegated handler in layout.html (locale-rendered, not cached) applies the accessible name + tooltip from the existing `code-copy`/`code-copied` strings and copies the sibling `<pre>` text on click (navigator.clipboard with the `__lcCopyFallback` fallback), flashing a checkmark + "Copied" for ~1.5s.

The handler is delegated on document and re-labels on htmx settle, so code blocks in live-arriving / edited messages and on the wiki / search surfaces all work. CSS reveals the button on hover / keyboard focus. Reused the pre-existing `code-copy`/`code-copied` i18n keys (en/es) rather than adding duplicates.

#LC-329
#LC-330

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-329-code-block-copy 2026-06-17 04:05:31 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!341
No description provided.