Rebuild the page actions row: Copy as markdown and Open in Claude - #3330
Draft
enf0rc3 wants to merge 11 commits into
Draft
Rebuild the page actions row: Copy as markdown and Open in Claude#3330enf0rc3 wants to merge 11 commits into
enf0rc3 wants to merge 11 commits into
Conversation
|
Pull request environment is available at https://stoctodocspr3330.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. |
enf0rc3
force-pushed
the
wl/open-in-llm
branch
2 times, most recently
from
August 11, 2026 02:41
3c7c692 to
4eb9610
Compare
The "Use Octopus docs with AI" pill offered three markdown actions. The design replaces it with a split button: a primary "Open in Claude", and a caret that opens a list of assistants. Each entry hands the assistant this page's .md URL to read. The markdown actions come back as a separate copy button once the shared copy module lands. Both halves reuse the .btn component, so the pair matches "Edit on GitHub" beside it in the page actions row, and the whole control moves from the foot of the article up under the header. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The split button hand-wrote `class="btn btn--small"`. Astro compiles Button.astro's `.btn` rules to `.btn[data-astro-cid-ekguhzzh]` and only stamps that attribute on elements Button itself renders, so none of the button styles reached this markup. Reuse the component instead of its class names. Button gains an `as` prop so a `<details>` disclosure trigger can be a real Button: a `<summary>` is a list item that carries its own marker, and dragging over a trigger selects its text rather than opening it, so both resets belong with the rest of the button styles. `<summary>` has no native disabled state, so `disabled` maps to `aria-disabled`, which the existing state rules already key off. Move the `.octo-llm` rules out of main.css into the component. Reaching into Button from here needs `:global()`, because the parent's scope attribute lands on the Button root but never on the icon spans inside it. Keeping `.octo-llm` in front of the `:global()` leaves the compiled selector scoped, so these rules cannot escape the component. Verified against the built output: the emitted CSS is unchanged by the pending move of button styles into an imported stylesheet, since that changes where the rules are authored and not what they compile to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page actions row lost its copy action when the AI dropdown became a split button, on the grounds that it would come back once the shared copy module landed. It has, so this brings it back as its own button. copy-button.js grows two things. A labelled button now shows its result in its label rather than a tooltip, which is what the design asks for and what an icon-only button cannot do. And copyFetchedOnClick covers text that has to be fetched: it hands ClipboardItem a pending promise so the click's user activation survives the request, which awaiting the fetch first would spend. The eligibility check both page actions need moves to pageMarkdownUrl, so neither component re-derives the slug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
globSync returns backslash-separated paths on Windows, and both call sites feed them into forward-slash logic. pathToSlug strips a trailing `/index`, so `argo-cd\index` kept its suffix and the page was written to dist/docs/argo-cd/index.md. Every request for /docs/argo-cd.md 404d, which took the page actions and llms.txt parity with it. The shared content lookup keys off path.posix.join, which leaves a backslash mid-key and never matches the include paths written in the source files. A page whose include failed to resolve was judged ineligible, so 1252 rather than 846 pages now get a .md companion on a Windows build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
enf0rc3
force-pushed
the
wl/open-in-llm
branch
from
August 11, 2026 02:49
4eb9610 to
10242e3
Compare
`unclipped` and `neighbour` were only in comments, and `hrefs` only a local name, so none of them needs a dictionary entry. `neighbour` in particular would have committed the repo to a British spelling that the dictionary does not otherwise establish. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The menu is wider than the control it hangs from, so the side it can open towards depends on where the control has ended up: at the end of the row on one line, or at the start of its own once the row wraps. A width breakpoint cannot tell those apart, and the one here had them backwards, so the menu forced a horizontal scrollbar between roughly 520 and 720 wide and was cut off past the start edge below 480. Anchor positioning asks the browser instead, and flip-inline covers the wrap. Fixed rather than absolute, because the containing block was the caret's `<details>` at 32px wide, which leaves the browser nothing to measure the room against; the anchor keeps it tracking the button through a scroll. Browsers without anchor positioning keep the plain absolute rule, now right-aligned rather than left, which is the orientation the unwrapped row needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both wrapped the same delegated listener, result swap and announcement around one differing line. That scaffolding moves to onCopyClick, which takes a function that starts the write. The write strategies stay apart. Handing ClipboardItem a pending promise only earns its keep when there is an await to survive; text already read off the page has none, so it keeps writeText, which is the simpler call and the more widely supported one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gemini's own web app ignores prompt parameters, so the entry had to point at AI Studio to prefill at all, which is not where "Open in Gemini" says it is going. Removes the entry, its label, its icon and the mask rule. Two shorter labels no longer fill 16rem, so the list is 13rem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page sets border-box on `html` but the universal reset does not pass it on, so `min-height` was measuring the content alone and the padding was landing outside it. Rows came out at 60px against a design that says 48, and the menu at 136px against 112. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows go from 3rem to 2.25rem and their block padding from space6 to space4, which takes the menu from 112px to 88px for two items. The design system has no compact MenuList yet, so the height borrows the step the medium button already uses rather than inventing a number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SplitButton is a design system component in its own right, described there as having the same anatomy and tokens as Button with the addition of a split action that triggers a Menu, so it composes Button the way the design does and takes the same icon, size and importance. OpenInLlm keeps only what is about assistants: the prompt, the two entries and their icons. The generic half moves with the component, including the Escape and outside-click handling, which was never LLM-specific. `max-width: fit-content` is new rather than moved. A flex or grid parent turns the root's `inline-flex` into a block-level box, which stretched it to the full column on the components page; `.btn` already guards against the same thing. 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.
Implements two designs from Documentation vision: Open in LLM and Copy to clipboard.
What changed
The "Use Octopus docs with AI" pill at the foot of the article is gone. Under the page header, next to Edit on GitHub, there are now two actions:
Copy as markdown — fetches this page's
.mdcompanion and puts it on the clipboard. Swaps to a check icon and "Copied" for two seconds, then reverts.Open in Claude with a caret — the primary half opens Claude with a prompt pointing at the page's
.mdURL. The caret opens a menu with Open in Claude and Open in ChatGPT, as link-styled rows with a provider logo and an external-link icon.Gemini is deliberately absent:
gemini.google.comignores prompt parameters, so the entry could only prefill by pointing at AI Studio, which is not where "Open in Gemini" says it is going.Both actions are the
Buttoncomponent, so borders, radius, hover, pressed and focus states come from the design-system tokens.Buttongains anasprop so the caret can be a<summary>; the split button's two halves collapse into one divider via a negative margin, which keeps each half's focus ring unclipped.Three icons added under
src/assets/icons/, exported from the Figma file and applied as CSS masks so they take--colorIconPrimaryin a button and--colorTextLinkDefaultin the menu.Reusing the shared copy module
copy-button.js(from #3317) grows two things rather than the copy action reimplementing them:showResultwrites to.btn__labelwhen there is one, and falls back to the tooltip otherwise. The design wants the result in the label; the two existing icon-only consumers are unchanged.copyFetchedOnClickcovers text that has to be fetched. It handsClipboardItema pending promise so the click's user activation survives the request — awaiting the fetch first and then writing loses it in Safari, which is the trap the module's own comment warns about.The delegated listener, result swap and announcement are shared by both entry points. The two write strategies stay apart on purpose: the promise trick only earns its keep when there is an await to survive, and text read off the page has none, so it keeps the simpler and more widely supported
writeText.The button also locks its measured width before the label shortens, so "Open in Claude" beside it doesn't shuffle. Measured rather than hardcoded, so it holds for any translation.
The eligibility check both actions need moved to
pageMarkdownUrl(), so neither component re-derives the slug.The menu opens towards whichever side has room
The menu is wider than the control, so the side it can open towards depends on where the control has ended up: at the end of the row on one line, or at the start of its own once the row wraps. A width breakpoint cannot tell those apart, and the first attempt had them backwards — the menu forced a horizontal scrollbar between roughly 520 and 720 wide, and was cut off past the start edge below 480.
CSS anchor positioning with
position-try-fallbacks: flip-inlineasks the browser instead.position: fixedrather than absolute, because the containing block was the caret's<details>at 32px wide, which leaves nothing to measure the room against; the anchor keeps it tracking the button through a scroll. Browsers without anchor positioning keep a plain absolute rule, right-aligned, which is the orientation the unwrapped row needs.Fixes a markdown emitter bug on Windows
globSyncreturns backslash-separated paths on Windows andllm-md-emitter.tsfed them into forward-slash logic in two places. Two lines ofposix: true:pathToSlugstrips a trailing/index, soargo-cd\indexkept its suffix and the page was written todist/docs/argo-cd/index.md. Every request for/docs/argo-cd.md404d, taking the page actions and llms.txt parity with it.path.posix.join, which leaves a backslash mid-key and never matches the include paths in the source files. A page whose include failed to resolve was judged ineligible, so 1252 rather than 846 pages now get a.mdcompanion on a Windows build.Linux was unaffected, which is why this survived. It is in this PR because it is what the two new actions point at.
Testing
astro buildfrom a cleandist: exit 0, 2673 pages, 1252.mdemitted.tests/llm-endpoints.spec.ts: 15 passed, 0 failed. Includes a test that clicks the copy button and reads the clipboard back to confirm the page markdown actually arrives, and two that open the menu at 700px and 430px and assert it stays on screen with no horizontal scrollbar. Before the emitter fix, five of these failed.Verified in a real browser in both themes: menu open and closed, the copy button through rest, copied and reverted, and the menu flipping side as the row wraps.
🤖 Generated with Claude Code