Give code blocks a header with a language and copy button - #3317
Open
enf0rc3 wants to merge 10 commits into
Open
Give code blocks a header with a language and copy button#3317enf0rc3 wants to merge 10 commits into
enf0rc3 wants to merge 10 commits into
Conversation
|
Pull request environment is available at https://stoctodocspr3317.z22.web.core.windows.net. You can view the ephemeral environment status in Octopus Deploy. This environment will be automatically deprovisioned when the pull request is closed, or after 7 days of inactivity. |
NES-285. Every fenced block is now wrapped in a shell carrying the block's label, its language and a copy button, matching the CodeBlock component in the design system. A group of <details data-group> panels that are each a lone code block becomes one block whose header offers the languages in a menu, replacing the tab list. Groups holding prose as well stay tabs. Blocks over 500px collapse behind a gradient fade until they are clicked. Text after the language on the opening fence becomes the block's label. Shiki drops that meta string, so a transformer keeps it on the element. The fences that only repeated their own language there have it removed, since that would render a label saying what the header already shows. Shiki now highlights against both themes, so dark mode gets real syntax colours in place of the inverting filter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow lints only the files a branch changed, so touching these surfaced 93 violations that were already there. None came from this branch: the same files on main report the same errors. Most were mechanical and went through markdownlint --fix. The rest needed a decision: - The expanded-properties table in certificate-variables was missing its trailing pipes and its third column, so twelve rows were losing data. It is rebuilt with every row filled in, and `header\footer` reads header/footer. - Six fences had no language. They are `text` now, with a label saying what the service message does. - output-variables used **PowerShell**, **C#**, **Bash**, **F#** and **Python3** as headings above their fences. The section heading already names the language and the block header now shows it, so the emphasis is gone and each fence carries a label instead. - The two certificate screenshots have alt text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The frame, header, label and language were being built by JavaScript after the page loaded. With scripting off, and in the window before hydration, a code block was bare text on the page background: the border, radius and padding used to sit on <pre> and now sit on the wrapper that script created. A Shiki transformer emits the whole shell instead, including the copy button. The copy handler is delegated at the document level, so it finds a statically rendered button by the same selector. Shiki, and not rehype, because plugins registered through `markdown.processor` never reach .mdx pages. rehypeWbr adds 18 <wbr> elements to the kubernetes-agent permissions page and none to kustomize.mdx, which has eight matches for it. code-blocks.js drops from 431 lines to 327: the wrapping, the copy button markup and the language table all go. It keeps copying, collapsing, and folding a <details data-group> set into one block with a language menu, which merges sibling blocks and so cannot be done per-block at build time. Two tests cover the shell with scripting disabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both had their own copy of the same fifty-five lines: the revert timer, the tooltip swap, the live region, and the delegated click. The only thing that differed was the string each one copies. copy-button.js takes a selector and a function that reads the text, so a caller is left with just that function. A button's own data-tooltip is its resting label, which keeps "Copy URL" on the heading and "Copy to clipboard" on the code block, and the two share one live region instead of one each. headers.js goes from 125 lines to 52, code-blocks.js from 327 to 256. copy-markdown.js stays as it is. It fetches the page over the network before writing, so it needs the execCommand fallback and cannot read its text synchronously, which is what keeps the clipboard write inside Safari's user activation. The heading button had no test. It has three now, covering both callers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The menu was a <details> with a hand-built option list, and eighteen of its lines re-implemented Escape-to-close and click-away-to-close. A <select> comes with those, plus keyboard navigation, focus handling and the mobile picker. code-blocks.js drops from 256 lines to 201, and the CSS loses the popup panel along with it. The trigger still matches the design. The list it opens is the browser's, so that part no longer matches the Figma panel: worth Mandy's eye before this merges. The caret is a mask on a wrapper span, since a <select> renders no pseudo-element of its own and a background image cannot follow the theme. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The switcher had its own caret-down.svg. The Button component and the copy markdown menu both draw theirs from the FontAwesome glyph, so this does too and the asset goes. Rebasing also turned up a conflict git could not see. Main now sets `margin-block` on `.page-content :is(pre, figure)`, and every <pre> sits inside a code block, so the code was pushed away from its own header. The rule points at .code-block instead, which is the element that wanted the spacing, and the block drops the 1rem it was setting for itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enf0rc3
force-pushed
the
wl/nes-285-code-block
branch
from
August 7, 2026 02:20
38710da to
1ab7325
Compare
A <details data-group> set with one member built a select holding one option, which is a control that cannot do anything. There is one such group in the docs. The block still loses its <details>, and its summary text becomes the fixed language. The transformer assigned over root.children, which would drop anything a later Shiki version put alongside the <pre>. It replaces the <pre> in place instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enf0rc3
marked this pull request as ready for review
August 7, 2026 02:53
Contributor
borland
reviewed
Aug 7, 2026
borland
left a comment
Contributor
There was a problem hiding this comment.
Looking at the code here, claude obviously wrote it all. At a high level it seems fine (no malware or bitcoin miners) but I'm not in position to nitpick the detail... and I don't think we need to either. I'd be happy to approve once you've taken a look at my other comment about text alignment and speed
.btn centres its text, and the caret needs the padding to be lopsided: 5px one side, 24px the other. A short language centred in what is left of the box landed about ten pixels off the button's own centre, reading as neither centred nor aligned. The box stays as wide as the longest language, so switching causes no reflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The old module made every <pre> focusable so a long line could be scrolled without a mouse. Nothing scrolls: `white-space: break-spaces` wraps the code, and none of the 43 blocks sampled across four pages overflow their box. The attribute still earns its place on a collapsed block, where the code is the only thing in the body that can take focus and focus is what opens it. So it goes on there and nowhere else, which is around 1,500 fewer tab stops. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fade was 59px and did not start until 60% down it, so it only really acted over the last 24px and the code read as simply ending. It is 7.5rem now and fades from its own top edge. Under it sits a Show more button that becomes Show less. That is what carries the message, and it is also the keyboard route in, so the <pre> no longer needs a tab stop and the focus handler that stood in for one is gone. Opening is one way apart from that button. Collapsing when a click landed elsewhere pulled the page up by the height of the block, which moved everything under the reader and lost their place. Collapsing from the button scrolls the block back into view for the same reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

NES-285 — the CodeBlock component from the Documentation vision file.
Every fenced code block gets a header carrying an optional label, its language, and a copy button.
What changed
The shell is static HTML.
src/plugins/shiki-code-block.jsis a Shiki transformer that emits the frame, header, label, language and copy button at build time, for all 1,599 fences across.mdand.mdx. With scripting off, a block still renders complete.Labels come from the fence meta.
```powershell Rename a deployment targetbecomes the block's label. Shiki drops that string, so the transformer keeps it. Most blocks have no label yet and render with the language and copy button alone — labelling the rest is content work for follow-up PRs.Several languages become a
<select>. A<details data-group>set whose panels are each a lone code block folds into one block with a language switcher. Groups holding prose as well stay tab lists.Long blocks collapse at 500px behind a gradient fade. Click the code to expand, click away to collapse.
Dark mode gets real syntax colours. Shiki now highlights against
light-plusanddark-plus, replacing theinvert(98%) hue-rotate(180deg)filter.One copy button module.
copy-button.jsbacks both the code block and the heading copy-URL button, which were carrying ~55 duplicated lines.headers.jsdrops from 125 lines to 52.Pages to test
/docs/kubernetes/steps/kustomize/docs/octopus-rest-api/octopus.client/using-resources.md/docs/octopus-rest-api/octopus.client/getting-started.mdx, long option label/docs/octopus-rest-api/octopus.server.exe-command-line/configure/docs/kubernetes/targets/kubernetes-agent/permissions/docs/projects/variables/certificate-variablesTry each in dark mode, and hover a heading to check the copy-URL button still behaves.
Worth a look
<select>. The trigger matches the design; the list it opens is the browser's, so that part diverges from the Figma panel. @mandymichael — is that acceptable?markdown.processoruntangled first.Also in here
Markdownlint runs only on changed files, so touching these surfaced 93 pre-existing violations. Same files on
mainreport the same 93. They are fixed: thecertificate-variablestable was genuinely broken, six fences had no language, andoutput-variablesused bold text as headings above its fences.Testing
tests/code-block.spec.tsandtests/copy-button.spec.ts— 12 tests covering the header, the label, copying, switching language, keyboard operation of the select, tabs staying tabs, collapse and expand, and the shell rendering with JavaScript disabled.Before/after