feat(ui): copy button on code blocks (LC-272) #314

Merged
longjacksonle merged 1 commit from feat/lc-272-code-copy-button into main 2026-06-14 06:21:57 +02:00

What

Adds a "Copy" button to each rendered code block (LC-272). Hovering or focusing a fenced code block reveals a button in the top-right that copies the raw code to the clipboard and briefly flips to "Copied" - no more hand-selecting multi-line snippets that scroll horizontally.

How

  • A persistent-shell JS enhancer (layout.html) wraps each .lc-md pre in a relative container and drops the button beside it (a sibling of the <pre>, not inside it, so the copied text is never polluted by the button's own label). It reads pre.textContent, which copies cleanly even for syntect-highlighted blocks (those carry styled spans and no inner <code>).
  • Reuses the LC-246 clipboard path: navigator.clipboard.writeText with the __lcCopyFallback legacy execCommand fallback.
  • Runs on load and on every htmx:afterSettle / htmx:oobAfterSwap, idempotent via a data-lc-code-done marker, so live-arriving and edited messages (and other markdown surfaces) get a button too.
  • main.css: .lc-code-wrap / .lc-code-copy (absolute top-right, revealed on hover/focus). Strings localized in en + es.

Progressive enhancement: with JS off the code still renders and is selectable. Pure front-end - no markdown/template render change, so no LC-77 fixture impact. No route/DB/env. Not operator-visible.

Tests

routes_drafts::page_ships_code_copy_enhancer asserts the page ships the enhancer + idempotency marker. just test and just test-saas pass.

Note

Caught one self-inflicted regression during the run: a code comment in the enhancer contained the literal string hx-swap-oob, which (since admin_layout.html extends layout.html) landed in the admin page body and tripped routes_quotas' !body.contains("hx-swap-oob") assertion. Reworded the comment; all green.

QA note

JS-driven, so worth a quick manual pass: post a fenced code block, hover it (Copy button appears top-right), click (copies the exact code, flips to "Copied"); confirm a highlighted block (e.g. ```rust) copies without color artifacts; confirm a code block in a live-arriving message and in a room wiki/description also gets the button; tab to the button to confirm it is focusable.

## What Adds a "Copy" button to each rendered code block (LC-272). Hovering or focusing a fenced code block reveals a button in the top-right that copies the raw code to the clipboard and briefly flips to "Copied" - no more hand-selecting multi-line snippets that scroll horizontally. ## How - A persistent-shell JS enhancer (`layout.html`) wraps each `.lc-md pre` in a relative container and drops the button beside it (a sibling of the `<pre>`, not inside it, so the copied text is never polluted by the button's own label). It reads `pre.textContent`, which copies cleanly even for syntect-highlighted blocks (those carry styled spans and no inner `<code>`). - Reuses the LC-246 clipboard path: `navigator.clipboard.writeText` with the `__lcCopyFallback` legacy `execCommand` fallback. - Runs on load and on every `htmx:afterSettle` / `htmx:oobAfterSwap`, idempotent via a `data-lc-code-done` marker, so live-arriving and edited messages (and other markdown surfaces) get a button too. - `main.css`: `.lc-code-wrap` / `.lc-code-copy` (absolute top-right, revealed on hover/focus). Strings localized in en + es. Progressive enhancement: with JS off the code still renders and is selectable. Pure front-end - no markdown/template render change, so no LC-77 fixture impact. No route/DB/env. Not operator-visible. ## Tests `routes_drafts::page_ships_code_copy_enhancer` asserts the page ships the enhancer + idempotency marker. `just test` and `just test-saas` pass. ## Note Caught one self-inflicted regression during the run: a code comment in the enhancer contained the literal string `hx-swap-oob`, which (since `admin_layout.html` extends `layout.html`) landed in the admin page body and tripped `routes_quotas`' `!body.contains("hx-swap-oob")` assertion. Reworded the comment; all green. ## QA note JS-driven, so worth a quick manual pass: post a fenced code block, hover it (Copy button appears top-right), click (copies the exact code, flips to "Copied"); confirm a highlighted block (e.g. ```rust) copies without color artifacts; confirm a code block in a live-arriving message and in a room wiki/description also gets the button; tab to the button to confirm it is focusable.
feat(ui): copy button on code blocks (LC-272)
All checks were successful
check-secrets / Nosey parker (push) Successful in 4s
Create release / Create release from merged PR (pull_request) Has been skipped
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Successful in 2m54s
cfd0d0fcb3
Adds a "Copy" button to each rendered code block. Hovering (or focusing) a fenced code block reveals a button in the top-right that copies the raw code to the clipboard and briefly flips to "Copied".

A persistent-shell enhancer wraps each .lc-md pre in a relative container and drops the button beside it (not inside, so the copied text is never polluted by the button), reading pre.textContent so syntect-highlighted blocks - which carry no inner <code> - copy cleanly. It reuses the LC-246 clipboard path (navigator.clipboard with the __lcCopyFallback legacy fallback). It runs on load and after every htmx settle / OOB swap, idempotent via a data-lc-code-done marker, so live-arriving and edited messages get a button too. Progressive enhancement: with JS off the code still renders and selects.

Pure front-end (no markdown/template render change, so no LC-77 fixture impact). Strings localized in en + es. Test asserts the page ships the enhancer.

#LC-272
#LC-273

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-272-code-copy-button 2026-06-14 06:21:57 +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!314
No description provided.