Skip to content

Commit 669fbc2

Browse files
committed
docs(fix[widgets/mcp-install]): prehydrate active state from <html> data attrs
The widget's server-rendered HTML hard-codes the first tab as aria-selected="true" and only the first panel un-hidden. widget.js, loaded blocking at end of <body>, then reads localStorage and mutates the DOM — visible flash on first paint when the saved selection differs from defaults. gp-sphinx's SPA navigation only swaps .article-container, so a head-only visibility gate (gp-sphinx's _inject_fowt_prevention pattern) would still flicker on every internal link click. Inject an inline <head> snippet via html-page-context that copies the saved selection from localStorage onto <html data-mcp-install-{client,method}> synchronously before paint, plus enumerated CSS rules (generated from CLIENTS/METHODS so they stay in lockstep with the data) that drive active tab styling and panel visibility from those attributes. <html> is never replaced by spa-nav.js, so the attributes survive SPA navigation and the new article paints in the saved state without the head script needing to re-run. widget.js mirrors the attribute on click so the CSS stays correct after user interaction.
1 parent 7505acc commit 669fbc2

3 files changed

Lines changed: 130 additions & 1 deletion

File tree

docs/_ext/widgets/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from ._directive import make_widget_directive
2828
from ._discovery import discover
29+
from ._prehydrate import inject_mcp_install_prehydrate
2930

3031
if t.TYPE_CHECKING:
3132
from sphinx.application import Sphinx
@@ -56,6 +57,7 @@ def setup(app: Sphinx) -> dict[str, t.Any]:
5657
"builder-inited",
5758
functools.partial(install_widget_assets, widgets=widgets),
5859
)
60+
app.connect("html-page-context", inject_mcp_install_prehydrate)
5961

6062
return {
6163
"version": __version__,

docs/_ext/widgets/_prehydrate.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Prevent flash-of-wrong-selection on the ``mcp-install`` widget.
2+
3+
The widget's server-rendered HTML always marks the first client/method tab
4+
``aria-selected="true"`` and ``hidden=""`` on every panel except the
5+
``(claude-code, uvx)`` cell. ``widget.js`` then reads ``localStorage`` and
6+
mutates the DOM to the user's saved selection — a visible flash on initial
7+
page paint and on every gp-sphinx SPA navigation between docs pages.
8+
9+
This module emits an inline ``<head>`` script that copies the saved selection
10+
from ``localStorage`` onto ``<html>`` as ``data-mcp-install-client`` /
11+
``data-mcp-install-method`` attributes *before first paint*, plus a ``<style>``
12+
block whose attribute-selector rules drive the active tab + visible panel
13+
from those attributes. ``<html>`` is never replaced by gp-sphinx's
14+
``spa-nav.js`` (it only swaps ``.article-container``), so the attributes
15+
survive SPA navigation and the new article paints in the saved state without
16+
the head script needing to re-run.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import typing as t
22+
23+
from .mcp_install import CLIENTS, METHODS
24+
25+
if t.TYPE_CHECKING:
26+
from sphinx.application import Sphinx
27+
28+
29+
_TAB_DEACTIVATE_RULE = (
30+
"html[data-mcp-install-client] .lm-mcp-install__tab"
31+
'[data-tab-kind="client"][aria-selected="true"],'
32+
"html[data-mcp-install-method] .lm-mcp-install__tab"
33+
'[data-tab-kind="method"][aria-selected="true"]'
34+
"{color:var(--color-foreground-muted);"
35+
"border-bottom-color:transparent;"
36+
"background:transparent}"
37+
)
38+
39+
_TAB_ACTIVE_DECL = (
40+
"{color:var(--color-brand-primary);"
41+
"border-bottom-color:var(--color-brand-primary);"
42+
"background:var(--color-background-primary)}"
43+
)
44+
45+
_PANEL_HIDE_RULE = (
46+
"html[data-mcp-install-client] .lm-mcp-install__panel:not([hidden])"
47+
"{display:none !important}"
48+
)
49+
50+
_PANEL_ACTIVE_DECL = "{display:block !important}"
51+
52+
_SCRIPT = (
53+
'<script data-cfasync="false">(function(){'
54+
"try{"
55+
"var h=document.documentElement;"
56+
'var c=localStorage.getItem("libtmux-mcp.mcp-install.client");'
57+
'var m=localStorage.getItem("libtmux-mcp.mcp-install.method");'
58+
'if(c)h.setAttribute("data-mcp-install-client",c);'
59+
'if(m)h.setAttribute("data-mcp-install-method",m);'
60+
"}catch(_){}"
61+
"})();</script>"
62+
)
63+
64+
65+
def _tab_active_selectors(kind: str, ids: tuple[str, ...]) -> str:
66+
return ",".join(
67+
f'html[data-mcp-install-{kind}="{id_}"] .lm-mcp-install__tab'
68+
f'[data-tab-kind="{kind}"][data-tab-value="{id_}"]'
69+
for id_ in ids
70+
)
71+
72+
73+
def _panel_active_selectors(
74+
client_ids: tuple[str, ...],
75+
method_ids: tuple[str, ...],
76+
) -> str:
77+
return ",".join(
78+
f'html[data-mcp-install-client="{c}"][data-mcp-install-method="{m}"]'
79+
f' .lm-mcp-install__panel[data-client="{c}"][data-method="{m}"]'
80+
for c in client_ids
81+
for m in method_ids
82+
)
83+
84+
85+
def _build_style() -> str:
86+
"""Return the ``<style>`` block that drives active state from html attrs.
87+
88+
Selectors are enumerated from :data:`CLIENTS` / :data:`METHODS` so adding
89+
a client or method auto-extends the prehydrate rules — no second source of
90+
truth to drift from.
91+
"""
92+
client_ids = tuple(c.id for c in CLIENTS)
93+
method_ids = tuple(m.id for m in METHODS)
94+
rules = [
95+
_TAB_DEACTIVATE_RULE,
96+
_tab_active_selectors("client", client_ids) + _TAB_ACTIVE_DECL,
97+
_tab_active_selectors("method", method_ids) + _TAB_ACTIVE_DECL,
98+
_PANEL_HIDE_RULE,
99+
_panel_active_selectors(client_ids, method_ids) + _PANEL_ACTIVE_DECL,
100+
]
101+
return "<style>" + "".join(rules) + "</style>"
102+
103+
104+
def _snippet() -> str:
105+
return _build_style() + _SCRIPT
106+
107+
108+
def inject_mcp_install_prehydrate(
109+
app: Sphinx,
110+
pagename: str,
111+
templatename: str,
112+
context: dict[str, t.Any],
113+
doctree: object,
114+
) -> None:
115+
"""Inject the prehydrate ``<style>`` + ``<script>`` into Furo's ``<head>``.
116+
117+
Appended to ``context["metatags"]`` so it lands in Furo's ``metatags`` slot
118+
(rendered before stylesheets and the ``<body>`` open). The pair is small
119+
(~1 KB) and a no-op when no widget is present, so we don't bother scoping
120+
to pages that use the directive.
121+
"""
122+
context["metatags"] = context.get("metatags", "") + _snippet()

docs/_widgets/mcp-install/widget.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@
7878

7979
updatePanels(widget);
8080

81-
if (opts.persist) localStorage.setItem(STORAGE[kind], value);
81+
if (opts.persist) {
82+
localStorage.setItem(STORAGE[kind], value);
83+
// Keep <html> attr in sync so the prehydrate CSS in _prehydrate.py
84+
// continues to drive active state across SPA navigations after a click.
85+
document.documentElement.setAttribute("data-mcp-install-" + kind, value);
86+
}
8287
if (opts.broadcast) {
8388
window.dispatchEvent(
8489
new CustomEvent(SYNC_EVENT, {

0 commit comments

Comments
 (0)