Skip to content
Merged
5 changes: 5 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ _Notes on upcoming releases will be added here_
- Bump gp-sphinx docs stack to v0.0.1a16 — docs site now renders
via `gp-furo-theme`, a Tailwind v4 respin of Furo, with
`sphinx-vite-builder` handling theme-asset builds (#33)
- Eliminate first-paint jank on the docs site: `{mcp-install}` no
longer flashes when restoring a saved client/method from
`localStorage` (CLS drops to 0), and the sidebar logo plus all
bold and italic text no longer pop in late as fonts arrive.
(#36)

## libtmux-mcp 0.1.0a4 (2026-05-02)

Expand Down
44 changes: 37 additions & 7 deletions docs/_ext/widgets/_prehydrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,29 @@
from sphinx.application import Sphinx


# Every prehydrate declaration is ``!important``. The whole block lives in
# ``@layer mcp-install-prehydrate`` (see :func:`_build_style`) and per CSS
# Cascade Level 5 only ``!important`` declarations get the layer-priority
# *reversal* that makes a layered rule outrank an unlayered one. Normal
# (non-``!important``) rules in a layer LOSE to unlayered rules of the same
# specificity — which is what bit the original tab rules: they were
# specific enough to beat ``widget.css``'s ``.tab[aria-selected="true"]``
# unlayered, but became powerless once we wrapped the prehydrate in a layer
# to fix the panel cascade against ``furo-tw``'s ``[hidden]`` preflight.
_TAB_DEACTIVATE_RULE = (
"html[data-mcp-install-client] .lm-mcp-install__tab"
'[data-tab-kind="client"][aria-selected="true"],'
"html[data-mcp-install-method] .lm-mcp-install__tab"
'[data-tab-kind="method"][aria-selected="true"]'
"{color:var(--color-foreground-muted);"
"border-bottom-color:transparent;"
"background:transparent}"
"{color:var(--color-foreground-muted) !important;"
"border-bottom-color:transparent !important;"
"background:transparent !important}"
)

_TAB_ACTIVE_DECL = (
"{color:var(--color-brand-primary);"
"border-bottom-color:var(--color-brand-primary);"
"background:var(--color-background-primary)}"
"{color:var(--color-brand-primary) !important;"
"border-bottom-color:var(--color-brand-primary) !important;"
"background:var(--color-background-primary) !important}"
)

_PANEL_HIDE_RULE = (
Expand Down Expand Up @@ -88,6 +97,27 @@ def _build_style() -> str:
Selectors are enumerated from :data:`CLIENTS` / :data:`METHODS` so adding
a client or method auto-extends the prehydrate rules — no second source of
truth to drift from.

Rules are wrapped in ``@layer mcp-install-prehydrate``. ``gp-furo-theme``
ships Tailwind v4's preflight inside ``@layer base``, including
``[hidden]:where(:not([hidden="until-found"])){display:none!important}``.
Per CSS Cascade Level 5, important-rule layer ordering is reversed:
rules in *any* cascade layer outrank ``!important`` unlayered rules
regardless of specificity. An unlayered prehydrate ``<style>`` therefore
loses to the preflight on the saved panel, so the saved panel paints as
``display:none`` until ``widget.js`` mutates ``[hidden]`` and the
install widget visibly grows. Declaring our rules in their own layer
makes them the *first* layer the browser encounters (the prehydrate
``<style>`` lives in ``metatags``, before any ``<link>``), which is
the highest-priority layer for ``!important``.

The reversal only applies to ``!important`` declarations. *Normal*
layered rules LOSE to *normal* unlayered rules — so every declaration
here is ``!important``, including the tab active/inactive colours
that competed (and won, unlayered) against ``widget.css``'s
``.lm-mcp-install__tab[aria-selected="true"]`` purely on specificity.
Drop the ``!important`` on a tab declaration and the active-tab
indicator will flash from server default to saved state on first paint.
"""
client_ids = tuple(c.id for c in CLIENTS)
method_ids = tuple(m.id for m in METHODS)
Expand All @@ -98,7 +128,7 @@ def _build_style() -> str:
_PANEL_HIDE_RULE,
_panel_active_selectors(client_ids, method_ids) + _PANEL_ACTIVE_DECL,
]
return "<style>" + "".join(rules) + "</style>"
return "<style>@layer mcp-install-prehydrate{" + "".join(rules) + "}</style>"


def _snippet() -> str:
Expand Down
23 changes: 23 additions & 0 deletions docs/_templates/page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{# Project-local override of gp-furo/base.html's logo prefetch hints.
gp-furo's default emits <link rel="prefetch"> for logos, which Chrome
treats as Lowest priority for a future navigation — so the sidebar
<img> first encounter triggers a fresh network request and the logo
pops in late. We use this logo on the current page, not a future one,
so rel="preload" is the correct hint. Also: light_logo and dark_logo
are the same file in this project, so emit a single tag (the browser
would dedupe URL-identical requests anyway, but two tags is noise).
#}
{% extends "!page.html" %}

{% block logo_prefetch_links %}
{%- if logo_url -%}
<link rel="preload" href="{{ logo_url }}" as="image">
{%- elif theme_light_logo and theme_dark_logo -%}
{%- set light_path = pathto('_static/' + theme_light_logo, 1) -%}
{%- set dark_path = pathto('_static/' + theme_dark_logo, 1) -%}
<link rel="preload" href="{{ light_path }}" as="image">
{%- if dark_path != light_path %}
<link rel="preload" href="{{ dark_path }}" as="image">
{%- endif -%}
{%- endif -%}
{% endblock %}
31 changes: 17 additions & 14 deletions docs/_widgets/mcp-install/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
* localStorage state is re-applied on DOMContentLoaded and on every
* gp-sphinx:navigated event (see sphinx-gp-theme's README for the contract).
*
* Visibility is fully CSS-driven by <html data-mcp-install-*> attrs and the
* @layer mcp-install-prehydrate rules in docs/_ext/widgets/_prehydrate.py.
* This script never mutates the panels' [hidden] attributes — it only
* keeps tab aria-selected and the <html> data-attrs in sync with the
* current selection. The CSS handles the rest.
*
* Vanilla JS, no deps.
*/
(function () {
Expand Down Expand Up @@ -76,13 +82,20 @@
});
if (!found) return; // value not available in this widget — ignore.

updatePanels(widget);
// Mirror current widget state onto <html> so _prehydrate.py's
// @layer mcp-install-prehydrate selectors drive panel visibility.
// Both attrs must be present for _panel_active to match, so set them
// unconditionally on every selection — including the click-before-saved
// case where the user picks a method without ever choosing a client
// (the other kind still has its server-default aria-selected to read).
var html = document.documentElement;
var clientValue = selectedValue(widget, "client");
var methodValue = selectedValue(widget, "method");
if (clientValue) html.setAttribute("data-mcp-install-client", clientValue);
if (methodValue) html.setAttribute("data-mcp-install-method", methodValue);

if (opts.persist) {
localStorage.setItem(STORAGE[kind], value);
// Keep <html> attr in sync so the prehydrate CSS in _prehydrate.py
// continues to drive active state across SPA navigations after a click.
document.documentElement.setAttribute("data-mcp-install-" + kind, value);
}
if (opts.broadcast) {
window.dispatchEvent(
Expand All @@ -93,16 +106,6 @@
}
}

function updatePanels(widget) {
var client = selectedValue(widget, "client");
var method = selectedValue(widget, "method");
widget.querySelectorAll(".lm-mcp-install__panel").forEach(function (panel) {
var match = panel.dataset.client === client && panel.dataset.method === method;
if (match) panel.removeAttribute("hidden");
else panel.setAttribute("hidden", "");
});
}

function selectedValue(widget, kind) {
var tab = widget.querySelector(
'.lm-mcp-install__tab[data-tab-kind="' + kind + '"][aria-selected="true"]'
Expand Down
22 changes: 22 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import typing as t

from gp_sphinx.config import make_linkcode_resolve, merge_sphinx_config
from gp_sphinx.defaults import DEFAULT_SPHINX_FONT_PRELOAD

import libtmux_mcp

Expand Down Expand Up @@ -64,6 +65,27 @@
rediraffe_redirects="redirects.txt",
copybutton_selector="div.highlight pre, div.admonition.prompt > p:last-child",
copybutton_exclude=".linenos, .admonition-title",
# gp-sphinx's default preloads only Sans 400/700 + Mono 400. furo-tw.css
# uses Sans 500 (h1-h6, sidebar labels, definition list terms) and 600
# (blockquote.epigraph). Without preload, those weights download lazily
# after first paint and the heading text visibly re-flows when the
# font finally lands (font-display: block hides the text for ~3s
# then swaps, so it reads as a flicker rather than fallback-then-real).
sphinx_font_preload=[
*DEFAULT_SPHINX_FONT_PRELOAD,
("IBM Plex Sans", 500, "normal"),
("IBM Plex Sans", 600, "normal"),
# The announcement bar contains <em>Pre-alpha.</em>, which
# resolves to 400 italic. Without preload, it loads ~54 ms
# after the preloaded normal weights and the announcement
# italic pops in late on first paint.
("IBM Plex Sans", 400, "italic"),
# Bold inline code (<strong><code>, ~14 hits on the index)
# uses Mono 700. Without preload, it loads ~30 ms after the
# other critical fonts and bold inline code transiently
# renders at fallback weight before swap.
("IBM Plex Mono", 700, "normal"),
],
)

conf["myst_enable_extensions"] = [*conf["myst_enable_extensions"], "attrs_inline"]
Expand Down