Skip to content

Space and indent embedded components inside lists - #3325

Merged
enf0rc3 merged 1 commit into
mainfrom
willlaugesen/embedded-component-spacing
Aug 10, 2026
Merged

Space and indent embedded components inside lists#3325
enf0rc3 merged 1 commit into
mainfrom
willlaugesen/embedded-component-spacing

Conversation

@enf0rc3

@enf0rc3 enf0rc3 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Implements the Embedded component design.

Figma wraps every non-text block embedded in the body — image, callout, table, code block — in a slot with space/8 above and below, on top of the space/16 block gap, and indents it with the list when it sits inside one.

Before After
Space above/below an embedded component in a list 0px 24px
Space above/below a table 16px 24px
Space above/below an image in a list 40px 24px
Paragraphs stacked in a list item 0px 16px
Nested list under an item 0px 6px
Table cell padding 0px 16px / 24px
Copy button offset from its code block 24px 0px

Indentation is 24px at list level 1 and 48px at level 2, matching the design.

Verified on the preview

Measured in the browser, light and dark:

Bugs found along the way

  • Table cells referenced --space-16 / --space-24 / --space-12. Those don't exist — the tokens are --space16 etc. — so every cell on every page with a markdown table rendered with no padding at all.
  • The copy button lives in a zero-height div injected in front of each code block, so the block's own top margin pushed the block away from its button instead of away from the text above it.
  • li:has(figure) spaced figures in list items with flex gap, which stacks with the margin instead of collapsing into it.

Checked, no change needed

The leading section (32px above the first paragraph, 56px above an h2, 12px below it, 16px between paragraphs) and the list (24px indent, no top padding on the first item, 6px between items) both already match main.

Out of scope: the code block header bar, which is #3317.

🤖 Generated with Claude Code

Figma wraps every non-text block embedded in the body in a slot with
space/8 above and below, on top of the space/16 block gap. That landed
for top-level images and code blocks, but the rule that spaces body
blocks uses a child combinator, so nothing inside a list item was
reached and code blocks sat flush against the text above them.

Tables were left out of the embedded set, and their cells referenced
--space-16 and --space-24, which don't exist - the tokens are --space16
and --space24 - so cells had no padding at all.

The copy button lives in a zero-height div injected in front of each
code block, so the block's own top margin was pushing the block away
from its button rather than away from the text above it.

li:has(figure) is gone: it was spacing figures in list items with flex
gap, which stacks with the margin instead of collapsing into it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@team-marketing-branch-protections

Copy link
Copy Markdown

Pull request environment is available at https://stoctodocspr3325.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.

@rosslovas

Copy link
Copy Markdown
Contributor

I noticed this PR added numbers to some lists that weren't there before. Example (note the new 4.):

Production:
image

Your PR:
image

What's going on here, was this intentional? It wasn't called out in the "bugs found along the way" in the PR description, but looking at the markdown this seems like a fix to me, these numbers probably should've been visible all along 🤔

@borland

borland commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

@rosslovas @enf0rc3 Yeah, that's super weird about the list now showing 4 when it didn't before. Looking at the source markdown file, there's a 4 in it, so the intent of the author was obviously for a number to be visible. This PR fixes a bug rather than creates one.

Viewing source on both prod and the staging site, it's identical, both are simply

<li>
<p>Give the new Private Endpoint a name and either accept or customize the generated Network Interface Name. Click “Next”.</p>
<figure><p><img src="/docs/img/octopus-cloud/images/create-private-endpoint-basics.png" alt="An example of how to fill in the basics tab while creating a private endpoint in the Azure Portal"></p></figure>
</li>

a) Why didn't we previously see it?
b) How can CSS-only change affect this?

I asked claude and it says this

If the item after the broken one shows 4 (as you describe), the browser isn't counting that <li> as a list item at all. If it showed 5, the item is still a list item and the marker is merely not being painted.

Case A — the count skips (marker never generated)

An <li> only gets a marker and only increments the list-item counter while its computed display is list-item. Anything that overrides that — display: block, flow-root, flex, grid — silently removes both. This is a very common casualty of spacing fixes, because display: flow-root and overflow: hidden are the standard tricks for containing collapsing child margins, and docs stylesheets often apply them to list items that contain block children like your

.

So look for a selector that matches only the items containing a figure — something like li:has(figure), li > figure ~ *, or a rule keyed off the image wrapper. Check the fourth <li> in DevTools: computed display should read list-item. I'd also check item 6, which has the same structure — I suspect it's broken too and you just noticed the first one.

Using browser devtools, the other li's show a ::marker pseudo element but this one doesn't, and sure enough in the main.css we have

.page-content {
  /* Heading gaps rely on sibling margin collapsing, which only happens in block
     flow. Converting this to flex or grid makes every gap additive again -
     56px above an h2 becomes 72px after a paragraph, 88px after a blockquote. */
  display: flow-root;
  padding-block-start: var(--space32);

  li:has(figure) {
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }
}

Which this PR has removed.

Nice fix. Approved

@enf0rc3
enf0rc3 merged commit 90fb96d into main Aug 10, 2026
7 checks passed
@enf0rc3
enf0rc3 deleted the willlaugesen/embedded-component-spacing branch August 10, 2026 22:11
enf0rc3 added a commit that referenced this pull request Aug 10, 2026
#3322 moved .btn into Button.astro's scoped style block, so it only reaches
markup that component renders. The copy button comes from the Shiki
transformer and the language select is built by script, so neither carries the
scoping attribute and both lost their styling in the rebase.

They carry the geometry themselves now, the same trade #3322 made for
.copy-heading-url: it can drift from <Button>, and that is worth more than
making a selector as broad as .btn global.

Also drops the .copy-container rules that arrived with #3325. That class went
with the old copy button this branch replaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enf0rc3 added a commit that referenced this pull request Aug 10, 2026
* Give code blocks a header with a language and copy button

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>

* Clear the markdownlint errors in the files this branch touches

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>

* Render the code block shell at build time

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>

* Share one copy-button module between the heading and the code block

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>

* Switch the language control to a native select

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>

* Use the caret the rest of the site already uses

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>

* Fix a switcher with nothing to switch, and a brittle transformer

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>

* Left-align the language in the switcher

.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>

* Give a tab stop only to code that can be opened with one

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>

* Put a Show more button under a collapsed block

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>

* Settle the Show more button, and land at the top when it closes

Secondary text, space12 of padding, and no rule above it.

Closing now puts the reader at the top of the block instead of near where it
was. block: 'nearest' left them wherever the collapsed height happened to run
out, which on a tall block is a different part of the page. Instant, because
animating a jump that size only reads as lag.

The block lands 140px down, which is the scroll-padding the site already sets
to clear the sticky header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Keep the collapse jump instant

The autofix on the pull request swapped behavior: 'instant' for 'auto'. 'auto'
defers to the CSS, and main.css sets scroll-behavior: smooth on html for anyone
without a reduced-motion preference, so the jump animated again.

'instant' is CSSOM View and has been in every current browser since 2023.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Give the code block controls their own button styles

#3322 moved .btn into Button.astro's scoped style block, so it only reaches
markup that component renders. The copy button comes from the Shiki
transformer and the language select is built by script, so neither carries the
scoping attribute and both lost their styling in the rebase.

They carry the geometry themselves now, the same trade #3322 made for
.copy-heading-url: it can drift from <Button>, and that is worth more than
making a selector as broad as .btn global.

Also drops the .copy-container rules that arrived with #3325. That class went
with the old copy button this branch replaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants