Skip to content

Commit 65e9ad4

Browse files
authored
docs(_static): restore prompt styling and SPA copy buttons (#16)
The gp-sphinx migration (#9) introduced two user-visible regressions this PR corrects. First, the project's admonition CSS was deleted, stripping the visual styling from every prompt / agent-thought / server-prompt block across the docs. Second, gp-sphinx's SPA router only re-attaches copy buttons to `div.highlight pre` after a DOM swap, leaving prompt-admonition copy buttons missing on pages like `/recipes/` (prompt-heavy, no code blocks) until a hard refresh. - **Restore admonition styling**: rules for `.admonition.prompt` (speech-bubble icon, accent border, italic body), `.admonition.agent-thought` (muted gray-bar narration), `span.prompt` (inline curly-quoted italics), and `div.system-prompt` / `div.server-prompt` (labeled dark code panels) lifted from the pre-migration `custom.css` into a new `docs/_static/css/project-admonitions.css`. - **SPA copy-button shim**: capture sphinx-copybutton's rendered `.copybtn` as a reusable template, then on every SPA swap re-insert buttons on `.admonition.prompt > p:last-child` nodes that lack one. Inserted buttons plug into ClipboardJS's body-delegated listener and `prompt-copy.js`'s capture-phase markdown-preserving handler transparently — no extra click listeners needed. - **Verified** end-to-end in real Chromium via Playwright: fresh loads render with sphinx-copybutton's own template; SPA round-trip to `/recipes/` restores the prompt copy buttons with `#mcp-promptcell-{N}` targets; code-only pages continue to route through gp-sphinx + ClipboardJS unchanged. Upstream follow-up: gp-sphinx's `spa-nav.js::addCopyButtons` should iterate the full `copybutton_selector` (or dispatch a `spa-nav-complete` event consumers can hook). The local shim retires when that lands.
2 parents 68cb272 + 581e80e commit 65e9ad4

3 files changed

Lines changed: 323 additions & 1 deletion

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/* Project-specific admonition styling for libtmux-mcp docs.
2+
*
3+
* These classes are *semantic* extensions added by the project and are not
4+
* shipped by the gp-sphinx theme, so they live here rather than upstream.
5+
* Restored from the pre-gp-sphinx-migration ``custom.css`` (commit c822f02^).
6+
*
7+
* .admonition.prompt — chat-bubble prompts (user → LLM)
8+
* .admonition.agent-thought — gray-bar agent chain-of-thought narration
9+
* span.prompt — inline italic prompt with curly quotes
10+
* div.system-prompt,
11+
* div.server-prompt — labeled dark code panels for AGENTS.md /
12+
* server-emitted prompts
13+
*/
14+
15+
/* ── Prompt blocks (user → LLM) ───────────────────────────
16+
* Styled admonition for copy-paste prompts. Chat-bubble
17+
* aesthetic with speech icon. Says "type this into your LLM."
18+
* ────────────────────────────────────────────────────────── */
19+
div.admonition.prompt {
20+
border: 1px solid color-mix(in srgb, var(--color-link) 25%, transparent);
21+
border-left: 3px solid var(--color-link);
22+
background: color-mix(in srgb, var(--color-link) 4%, var(--color-background-primary));
23+
border-radius: 6px;
24+
padding: 0.75rem 1rem 0.75rem 2.2rem;
25+
margin: 1.25rem 0;
26+
box-shadow: none;
27+
position: relative;
28+
}
29+
30+
/* Speech bubble icon */
31+
div.admonition.prompt::before {
32+
content: "\1F4AC";
33+
position: absolute;
34+
left: 0.65rem;
35+
top: 0.7rem;
36+
font-size: 0.85rem;
37+
line-height: 1;
38+
opacity: 0.45;
39+
}
40+
41+
div.admonition.prompt > .admonition-title {
42+
display: none;
43+
}
44+
45+
div.admonition.prompt > p {
46+
font-size: 0.95rem;
47+
font-style: italic;
48+
color: var(--color-foreground-primary);
49+
}
50+
51+
div.admonition.prompt > p:last-of-type {
52+
margin-bottom: 0;
53+
}
54+
55+
/* Copy button — inserted afterend of p:last-child, so it
56+
* lands inside the prompt div. position:relative on the
57+
* prompt div provides the positioning context. */
58+
div.admonition.prompt > button.copybtn {
59+
background: transparent !important;
60+
cursor: pointer;
61+
border: none !important;
62+
}
63+
64+
div.admonition.prompt:hover > button.copybtn,
65+
div.admonition.prompt > button.copybtn.success {
66+
opacity: 1;
67+
}
68+
69+
div.admonition.prompt > p:last-child {
70+
margin-bottom: 0;
71+
}
72+
73+
/* ── Agent reasoning blocks ──────────────────────────────
74+
* Styled admonition for agent chain-of-thought. Neutral
75+
* gray bar + italic + muted opacity = "internal narration,
76+
* not something you do."
77+
* ────────────────────────────────────────────────────────── */
78+
div.admonition.agent-thought {
79+
border: none;
80+
border-left: 3px solid var(--color-foreground-border);
81+
background: transparent;
82+
padding: 0.6rem 1rem;
83+
margin: 1rem 0;
84+
box-shadow: none;
85+
}
86+
87+
div.admonition.agent-thought > .admonition-title {
88+
display: none;
89+
}
90+
91+
div.admonition.agent-thought > p {
92+
font-style: italic;
93+
color: var(--color-foreground-secondary);
94+
font-size: 0.9rem;
95+
}
96+
97+
div.admonition.agent-thought > p:last-child {
98+
margin-bottom: 0;
99+
}
100+
101+
/* ── Inline prompt dialog ────────────────────────────────
102+
* Inline styled span for user-to-LLM prompts. Supports
103+
* full nested markup via MyST attrs_inline extension:
104+
* [Run `pytest` in my build pane]{.prompt}
105+
* No line-height or word-wrap disruption. WCAG AA contrast.
106+
* ────────────────────────────────────────────────────────── */
107+
span.prompt {
108+
font-style: italic;
109+
color: var(--color-foreground-primary);
110+
}
111+
112+
span.prompt::before {
113+
content: "\201C";
114+
color: var(--color-foreground-muted);
115+
}
116+
117+
span.prompt::after {
118+
content: "\201D";
119+
color: var(--color-foreground-muted);
120+
}
121+
122+
/* ── Labeled code panels ─────────────────────────────────
123+
* Copyable prose in labeled dark panels. Two variants:
124+
* .system-prompt — user-authored fragments for AGENTS.md
125+
* .server-prompt — libtmux-mcp's built-in instructions
126+
* Keeps sphinx-copybutton via .highlight > pre selector.
127+
* ────────────────────────────────────────────────────────── */
128+
div.system-prompt,
129+
div.server-prompt {
130+
margin: 1.25rem 0;
131+
position: relative;
132+
}
133+
134+
div.system-prompt > div.highlight,
135+
div.server-prompt > div.highlight {
136+
background: #1f2329 !important;
137+
border: 1px solid color-mix(in srgb, var(--color-link) 20%, transparent);
138+
border-left: 3px solid var(--color-link);
139+
border-radius: 6px;
140+
position: relative;
141+
padding-top: 1.3rem;
142+
}
143+
144+
div.system-prompt > div.highlight > pre,
145+
div.server-prompt > div.highlight > pre {
146+
background: transparent;
147+
font-size: 13px;
148+
line-height: 1.6;
149+
white-space: pre-wrap;
150+
word-break: break-word;
151+
}
152+
153+
/* Internal label — quiet uppercase whisper */
154+
div.system-prompt > div.highlight::before,
155+
div.server-prompt > div.highlight::before {
156+
position: absolute;
157+
top: 0.45rem;
158+
left: 0.85rem;
159+
font-family: var(--font-stack);
160+
font-size: 0.55rem;
161+
font-weight: 500;
162+
letter-spacing: 0.05em;
163+
text-transform: uppercase;
164+
color: #9590b8;
165+
background: transparent;
166+
padding: 0;
167+
line-height: 1;
168+
opacity: 0.8;
169+
}
170+
171+
div.system-prompt > div.highlight::before {
172+
content: "System prompt";
173+
}
174+
175+
div.server-prompt > div.highlight::before {
176+
content: "Built-in server prompt";
177+
}
178+
179+
/* Fix copy button background to match panel */
180+
div.system-prompt .copybtn,
181+
div.server-prompt .copybtn {
182+
background: #1f2329 !important;
183+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Re-attach copy buttons to ``.admonition.prompt > p:last-child`` after
3+
* gp-sphinx's SPA DOM swap.
4+
*
5+
* Context:
6+
*
7+
* - ``copybutton_selector`` in ``docs/conf.py`` is
8+
* ``"div.highlight pre, div.admonition.prompt > p:last-child"`` — we copy
9+
* *prompt text* as well as code.
10+
* - On full-page load, ``sphinx-copybutton`` iterates that selector and
11+
* inserts a ``.copybtn`` after every match, then binds
12+
* ``new ClipboardJS('.copybtn', ...)``. ClipboardJS uses delegated
13+
* listening on ``document.body``, so those clicks keep working across
14+
* SPA DOM swaps.
15+
* - On SPA navigation, gp-sphinx's ``spa-nav.js::addCopyButtons`` iterates
16+
* ``"div.highlight pre"`` only — it does NOT re-attach buttons to
17+
* ``.admonition.prompt > p:last-child``. After an SPA swap, pages like
18+
* ``/recipes/`` (prompt-heavy, no code blocks) render naked: no copy
19+
* affordance at all.
20+
*
21+
* This shim: capture the first ``.copybtn`` that appears anywhere in the
22+
* document as a reusable template (so we pick up ``sphinx-copybutton``'s
23+
* locale-specific tooltip and icon exactly), then after every SPA swap
24+
* re-insert buttons on prompt-admonition ``<p>`` elements that lack a
25+
* ``.copybtn`` sibling. Because the inserted elements have
26+
* ``class="copybtn"`` and a ``data-clipboard-target`` pointing to a
27+
* ``<p>`` with a matching ``id``, they plug into ClipboardJS's
28+
* body-delegated listener transparently and behave identically to
29+
* initially-rendered buttons.
30+
*
31+
* ``FALLBACK_COPYBTN_HTML`` covers the rare case where the user's first
32+
* page has no ``.copybtn`` anywhere (e.g. a landing page with no code
33+
* blocks and no prompt admonitions) — the fallback button is a bare
34+
* ``.copybtn`` with the same MDI "content-copy" icon upstream
35+
* ``sphinx-copybutton`` ships. Ugly if tooltip styling needs the exact
36+
* template but functional for clicks.
37+
*
38+
* The correct upstream fix is in gp-sphinx — its ``addCopyButtons``
39+
* should iterate the full ``copybutton_selector`` (or dispatch a
40+
* ``spa-nav-complete`` event that consumers like ``sphinx-copybutton``
41+
* can hook). Until then, this project-local shim keeps the docs
42+
* behaving.
43+
*/
44+
(function () {
45+
"use strict";
46+
47+
if (!window.MutationObserver) return;
48+
49+
var PROMPT_TARGET = ".admonition.prompt > p:last-child";
50+
var FALLBACK_COPYBTN_HTML =
51+
'<button class="copybtn o-tooltip--left" data-tooltip="Copy">' +
52+
'<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">' +
53+
'<title>Copy</title>' +
54+
'<path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"/>' +
55+
"</svg></button>";
56+
57+
var copyBtnTemplate = null;
58+
var idCounter = 0;
59+
60+
function ensureTemplate() {
61+
if (copyBtnTemplate) return true;
62+
var live = document.querySelector(".copybtn");
63+
if (live) {
64+
copyBtnTemplate = live.cloneNode(true);
65+
copyBtnTemplate.classList.remove("success");
66+
copyBtnTemplate.removeAttribute("data-clipboard-target");
67+
return true;
68+
}
69+
// Fallback: no live .copybtn on page — fabricate from known markup.
70+
var holder = document.createElement("div");
71+
holder.innerHTML = FALLBACK_COPYBTN_HTML;
72+
copyBtnTemplate = holder.firstChild;
73+
return true;
74+
}
75+
76+
function ensurePromptButtons() {
77+
if (!ensureTemplate()) return;
78+
document.querySelectorAll(PROMPT_TARGET).forEach(function (p) {
79+
var next = p.nextElementSibling;
80+
if (next && next.classList && next.classList.contains("copybtn")) {
81+
return;
82+
}
83+
if (!p.id) {
84+
p.id = "mcp-promptcell-" + idCounter;
85+
idCounter += 1;
86+
}
87+
var btn = copyBtnTemplate.cloneNode(true);
88+
btn.classList.remove("success");
89+
btn.setAttribute("data-clipboard-target", "#" + p.id);
90+
p.insertAdjacentElement("afterend", btn);
91+
});
92+
}
93+
94+
// Observer has two jobs:
95+
// (a) capture the template the instant sphinx-copybutton inserts its
96+
// first ``.copybtn`` (happens at DOMContentLoaded, regardless of
97+
// listener-registration order vs our own);
98+
// (b) detect SPA-swap completion (a subtree addition that contains a
99+
// ``.admonition.prompt``) and re-insert prompt buttons.
100+
new MutationObserver(function (records) {
101+
var sawCopybtn = false;
102+
var sawArticle = false;
103+
for (var i = 0; i < records.length; i += 1) {
104+
var added = records[i].addedNodes;
105+
for (var j = 0; j < added.length; j += 1) {
106+
var n = added[j];
107+
if (n.nodeType !== 1) continue;
108+
var cls = n.classList;
109+
if (cls && cls.contains("copybtn")) sawCopybtn = true;
110+
if (cls && cls.contains("admonition") && cls.contains("prompt")) {
111+
sawArticle = true;
112+
}
113+
if (n.querySelector) {
114+
if (!sawCopybtn && n.querySelector(".copybtn")) sawCopybtn = true;
115+
if (!sawArticle && n.querySelector(".admonition.prompt")) {
116+
sawArticle = true;
117+
}
118+
}
119+
}
120+
}
121+
if (sawCopybtn) ensureTemplate();
122+
if (sawArticle) ensurePromptButtons();
123+
}).observe(document.body, { childList: true, subtree: true });
124+
125+
// Initial-load pass — MUST run after sphinx-copybutton has had its own
126+
// DOMContentLoaded handler attach its buttons, otherwise our fallback
127+
// template beats sphinx-copybutton's localized one to the punch on
128+
// prompt-only pages like ``/recipes/``. At deferred-script execution
129+
// time ``readyState`` is ``"interactive"`` (parse done, DOMContentLoaded
130+
// not yet fired), so register a listener instead of running eagerly.
131+
// ``"complete"`` means everything has already fired — safe to run now.
132+
if (document.readyState === "complete") {
133+
ensurePromptButtons();
134+
} else {
135+
document.addEventListener("DOMContentLoaded", ensurePromptButtons);
136+
}
137+
})();

docs/conf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,12 @@ def _convert_md_xrefs(
125125

126126

127127
def setup(app: Sphinx) -> None:
128-
"""Configure Sphinx app hooks and register project-specific JS."""
128+
"""Configure Sphinx app hooks and register project-specific JS/CSS."""
129129
_gp_setup(app)
130130
app.connect("autodoc-process-docstring", _convert_md_xrefs)
131131
app.add_js_file("js/prompt-copy.js", loading_method="defer")
132+
app.add_js_file("js/spa-copybutton-reinit.js", loading_method="defer")
133+
app.add_css_file("css/project-admonitions.css")
132134

133135

134136
globals().update(conf)

0 commit comments

Comments
 (0)