From b742f06eaf91b2e2a57eeebe82f40707a0a8875b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 13 Feb 2026 22:57:45 +1000 Subject: [PATCH 01/10] Docs theme-v2: refine nav/TOC styling and add code visibility controls --- docs/_static/custom.css | 595 +++++++++++++++++++++++++++++++++++++--- docs/_static/custom.js | 281 +++++++++++++++++++ docs/conf.py | 106 +++++-- 3 files changed, 925 insertions(+), 57 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 145657869..a337a959d 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,8 +1,238 @@ +:root { + /* Core surfaces */ + --uplt-color-panel-bg: #ffffff; /* page bg (light) */ + --uplt-color-sidebar-bg: #f4f4f4; /* TOC + notebook cell bg (light) */ + --uplt-color-card-bg: #f4f4f4; /* used by .card-img-top background */ + --uplt-color-white: #ffffff; + + /* Borders & shadows */ + --uplt-color-border-muted: #e1e4e5; + --uplt-color-button-border: #c5c5c5; + --uplt-color-shadow: rgba(0, 0, 0, 0.1); + + /* Text */ + --uplt-color-text-main: #404040; + --uplt-color-text-strong: #333333; + --uplt-color-text-secondary: #555555; + --uplt-color-text-muted: #606060; + + /* Accent */ + --uplt-color-accent: #0f766e; + --uplt-color-accent-hover: rgba(15, 118, 110, 0.1); + --uplt-color-accent-active: rgba(15, 118, 110, 0.15); + --uplt-color-accent-grad-start: rgba(15, 118, 110, 0.1); + --uplt-color-accent-grad-end: rgba(15, 118, 110, 0.02); + --uplt-color-accent-shadow-strong: rgba(15, 118, 110, 0.2); + --uplt-color-accent-shadow-soft: rgba(15, 118, 110, 0.1); + + /* Scrollbar */ + --uplt-color-scrollbar-track: #f1f1f1; + --uplt-color-scrollbar-thumb: #cdcdcd; + --uplt-color-scrollbar-thumb-hover: #9e9e9e; + + --uplt-color-code-bg: var(--uplt-color-sidebar-bg); /* same as page */ + --uplt-color-code-fg: #6a6a6a; /* gray code text (light) */ + --code-block-background: var(--uplt-color-code-bg) !important; + --sy-c-link: var(--uplt-color-accent); + --sy-c-link-hover: #0b5f59; + --uplt-color-toc-bg: #e9e9e9; +} + +.sy-main .yue a, +.globaltoc a, +.localtoc a, +.sy-breadcrumbs a { + color: var(--sy-c-link); +} + +.sy-main .yue a:hover, +.globaltoc a:hover, +.localtoc a:hover, +.sy-breadcrumbs a:hover { + color: var(--sy-c-link-hover); +} + +.sy-main .yue a:not(.headerlink) { + border-bottom-color: transparent !important; + text-decoration: none !important; +} + +.sy-main .yue a:not(.headerlink):hover { + border-bottom-color: transparent !important; + text-decoration: underline !important; + text-decoration-color: var(--sy-c-link-hover); + text-decoration-thickness: 0.08em; + text-underline-offset: 0.14em; +} + +.sy-head .sy-head-links { + justify-content: flex-start !important; + column-gap: 1.8rem !important; + padding-left: 1.25rem !important; + padding-right: 1.25rem !important; +} + +@media (min-width: 768px) { + .sy-head .sy-head-links > ul { + display: flex !important; + align-items: center; + justify-content: flex-start; + column-gap: 2.8rem !important; + margin: 0 !important; + padding: 0 !important; + text-align: left; + } + + .sy-head .sy-head-links > ul > li.link { + margin: 0 !important; + padding: 0 !important; + } +} + +.sy-head .sy-head-links a { + border: 0 !important; + border-bottom: 2px solid transparent !important; + border-radius: 0; + padding: 0.2rem 0.04rem 0.2rem; + line-height: 1.25; + font-size: 0.74rem; + font-weight: 600; + letter-spacing: 0.065em; + text-transform: uppercase; + color: var(--uplt-color-text-main); + background: transparent !important; + text-decoration: none !important; + transition: + border-bottom-color 0.2s ease, + color 0.2s ease, + opacity 0.2s ease; +} + +.sy-head .sy-head-links a:hover { + border-bottom-color: rgba(15, 118, 110, 0.35) !important; + color: var(--uplt-color-accent); + opacity: 1; +} + +.sy-head .sy-head-links a[href="#"], +.sy-head .sy-head-links a[aria-current="page"] { + color: var(--uplt-color-accent) !important; + border-bottom-color: var(--uplt-color-accent) !important; + opacity: 1; +} + +/* Content heading hierarchy */ +.sy-main .yue h1 { + font-size: clamp(2rem, 2.6vw, 2.5rem); + line-height: 1.12; + font-weight: 700; + letter-spacing: -0.01em; + margin: 0 0 1rem; + color: var(--sy-c-heading); +} + +.sy-main .yue h2 { + font-size: clamp(1.35rem, 1.8vw, 1.65rem); + line-height: 1.25; + font-weight: 650; + margin: 2.2rem 0 0.8rem; + padding-bottom: 0.35rem; + border-bottom: 1px solid var(--sy-c-divider); + box-shadow: inset 0 -2px 0 0 var(--uplt-color-accent-hover); + color: var(--sy-c-heading); +} + +.sy-main .yue h3 { + font-size: 1.08rem; + line-height: 1.3; + font-weight: 620; + margin: 1.45rem 0 0.5rem; + padding-left: 0.55rem; + border-left: 3px solid var(--uplt-color-accent); + color: var(--sy-c-heading); +} + +.sy-main .yue h4, +.sy-main .yue h5, +.sy-main .yue h6 { + font-size: 0.98rem; + font-weight: 600; + margin: 1.1rem 0 0.35rem; + color: var(--sy-c-text); +} + +html.dark .sy-head .sy-head-links a, +html.dark-theme .sy-head .sy-head-links a, +[data-color-mode="dark"] .sy-head .sy-head-links a { + color: #dbe6e5; + opacity: 0.96; +} + +html.dark .sy-head .sy-head-links a:hover, +html.dark-theme .sy-head .sy-head-links a:hover, +[data-color-mode="dark"] .sy-head .sy-head-links a:hover { + color: #66d0c6; + border-bottom-color: rgba(102, 208, 198, 0.55) !important; +} + +html.dark .sy-head .sy-head-links a[href="#"], +html.dark-theme .sy-head .sy-head-links a[href="#"], +[data-color-mode="dark"] .sy-head .sy-head-links a[href="#"], +html.dark .sy-head .sy-head-links a[aria-current="page"], +html.dark-theme .sy-head .sy-head-links a[aria-current="page"], +[data-color-mode="dark"] .sy-head .sy-head-links a[aria-current="page"] { + color: #8be0d9 !important; + border-bottom-color: #8be0d9 !important; +} + +@media screen and (max-width: 1200px) { + .sy-head .sy-head-links { + column-gap: 3.8rem !important; + padding-left: 1rem !important; + padding-right: 1rem !important; + } +} + +.yue :not(pre) > code, +.yue code.docutils.literal.notranslate, +.yue code.docutils.literal.notranslate .pre { + color: var(--sy-c-link); + background-color: var(--uplt-color-accent-hover); + border: 1px solid var(--uplt-color-border-muted); + border-radius: 0.2rem; + padding: 0.06rem 0.28rem; +} + +html.dark, +html.dark-theme, +[data-color-mode="dark"] { + --uplt-color-accent: #1aa89a; + --uplt-color-accent-hover: rgba(26, 168, 154, 0.14); + --uplt-color-accent-active: rgba(26, 168, 154, 0.22); + --uplt-color-accent-grad-start: rgba(26, 168, 154, 0.16); + --uplt-color-accent-grad-end: rgba(26, 168, 154, 0.04); + --uplt-color-accent-shadow-strong: rgba(26, 168, 154, 0.26); + --uplt-color-accent-shadow-soft: rgba(26, 168, 154, 0.14); + --sy-c-link: #58d5c9; + --sy-c-link-hover: #84e8df; + --uplt-color-panel-bg: #202020; + --code-block-background: #2a2a2a; + --uplt-color-toc-bg: #171717; +} + +@media (prefers-color-scheme: dark) { + html:not(.light):not(.light-theme):not([data-color-mode="light"]) { + --uplt-color-panel-bg: #202020; + --code-block-background: #2a2a2a; + --uplt-color-toc-bg: #171717; + } +} + .grid-item-card .card-img-top { height: 100%; object-fit: cover; width: 100%; - background-color: slategrey; + background-color: var(--uplt-color-card-bg); } /* Make all cards with this class use flexbox for vertical layout */ @@ -43,13 +273,13 @@ justify-content: space-between; align-items: center; padding: 12px 15px; - border-bottom: 1px solid #e1e4e5; + border-bottom: 1px solid var(--uplt-color-border-muted); } .right-toc-title { font-weight: 600; font-size: 1.1em; - color: #2980b9; + color: var(--uplt-color-accent); } .right-toc-buttons { @@ -60,7 +290,7 @@ .right-toc-toggle-btn { background: none; border: none; - color: #2980b9; + color: var(--uplt-color-accent); font-size: 16px; cursor: pointer; width: 24px; @@ -74,7 +304,7 @@ } .right-toc-toggle-btn:hover { - background-color: rgba(41, 128, 185, 0.1); + background-color: var(--uplt-color-accent-hover); } .right-toc-content { @@ -93,16 +323,16 @@ display: block; padding: 5px 0; text-decoration: none; - color: #404040; + color: var(--uplt-color-text-main); border-radius: 4px; transition: all 0.2s ease; margin-bottom: 3px; } .right-toc-link:hover { - background-color: rgba(41, 128, 185, 0.1); + background-color: var(--uplt-color-accent-hover); padding-left: 5px; - color: #2980b9; + color: var(--uplt-color-accent); } .right-toc-level-h1 { @@ -118,13 +348,13 @@ .right-toc-level-h3 { padding-left: 2.4em; font-size: 0.9em; - color: #606060; + color: var(--uplt-color-text-muted); } .right-toc-subtoggle { background: none; border: none; - color: #2980b9; + color: var(--uplt-color-accent); cursor: pointer; font-size: 0.9em; margin-right: 0.3em; @@ -139,8 +369,8 @@ /* Active TOC item highlighting */ .right-toc-link.active { - background-color: rgba(41, 128, 185, 0.15); - color: #2980b9; + background-color: var(--uplt-color-accent-active); + color: var(--uplt-color-accent); font-weight: 500; padding-left: 5px; } @@ -162,17 +392,17 @@ } .right-toc-content::-webkit-scrollbar-track { - background: #f1f1f1; + background: var(--uplt-color-scrollbar-track); border-radius: 10px; } .right-toc-content::-webkit-scrollbar-thumb { - background: #cdcdcd; + background: var(--uplt-color-scrollbar-thumb); border-radius: 10px; } .right-toc-content::-webkit-scrollbar-thumb:hover { - background: #9e9e9e; + background: var(--uplt-color-scrollbar-thumb-hover); } .toc-wrapper { @@ -192,11 +422,11 @@ width: 280px; left: 1125px; font-size: 0.9em; - background-color: #f8f9fa; + background-color: var(--uplt-color-panel-bg); z-index: 100; border-radius: 6px; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); - border-left: 3px solid #2980b9; + box-shadow: 0 4px 10px var(--uplt-color-shadow); + border-left: 3px solid var(--uplt-color-accent); transition: all 0.3s ease; max-height: calc(100vh - 150px); } @@ -207,12 +437,12 @@ border-radius: 16px; background: linear-gradient( 135deg, - rgba(41, 128, 185, 0.08), - rgba(41, 128, 185, 0.02) + var(--uplt-color-accent-grad-start), + var(--uplt-color-accent-grad-end) ); box-shadow: - 0 10px 24px rgba(41, 128, 185, 0.18), - 0 2px 6px rgba(41, 128, 185, 0.08); + 0 10px 24px var(--uplt-color-accent-shadow-strong), + 0 2px 6px var(--uplt-color-accent-shadow-soft); } .gallery-filter-bar { @@ -223,9 +453,9 @@ } .gallery-filter-button { - border: 1px solid #c5c5c5; - background-color: #ffffff; - color: #333333; + border: 1px solid var(--uplt-color-button-border); + background-color: var(--uplt-color-white); + color: var(--uplt-color-text-strong); padding: 0.35rem 0.85rem; border-radius: 999px; font-size: 0.9em; @@ -237,9 +467,9 @@ } .gallery-filter-button.is-active { - background-color: #2980b9; - border-color: #2980b9; - color: #ffffff; + background-color: var(--uplt-color-accent); + border-color: var(--uplt-color-accent); + color: var(--uplt-color-white); } .gallery-section-hidden { @@ -332,14 +562,58 @@ body.wy-body-for-nav font-weight: bold; display: block; margin: 1.5em 0 0.5em 0; - border-bottom: 2px solid #2980b9; + border-bottom: 2px solid var(--uplt-color-accent); padding-bottom: 0.3em; - color: #2980b9; + color: var(--uplt-color-accent); } .gallery-section-description { margin: 0 0 1em 0; - color: #555; + color: var(--uplt-color-text-secondary); +} + +/* Gallery example pages: collapsible source code */ +.yue details.uplt-code-details { + margin-top: 0.9rem; + border: 1px solid var(--uplt-color-border-muted); + border-radius: 0.35rem; + background: var(--uplt-color-panel-bg); +} + +.yue details.uplt-code-details > summary.uplt-code-summary { + list-style: none; + cursor: pointer; + user-select: none; + padding: 0.45rem 0.7rem; + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.03em; + color: var(--sy-c-link); +} + +.yue details.uplt-code-details > summary.uplt-code-summary::-webkit-details-marker { + display: none; +} + +.yue details.uplt-code-details[open] > summary.uplt-code-summary { + border-bottom: 1px solid var(--uplt-color-border-muted); +} + +.yue details.uplt-code-details > .highlight-Python { + margin: 0; + border: 0; + border-radius: 0 0 0.35rem 0.35rem; +} + +.yue details.uplt-code-details > .nbinput.docutils.container { + margin: 0; + border: 0; + border-radius: 0 0 0.35rem 0.35rem; +} + +.yue details.uplt-code-details > .nbinput.docutils.container div.input_area { + border-radius: 0 0 0.35rem 0.35rem; + border-top: 0; } /* Responsive adjustments */ @@ -365,3 +639,262 @@ body.wy-body-for-nav height: auto; display: block; } + +/* Shibuya: unify sidebar and notebook cell backgrounds */ +.sy-lside, +.sy-lside-inner, +.sy-rside, +.sy-rside-inner, +.sy-scrollbar { + background-color: var(--uplt-color-toc-bg); +} + +/*.yue div.nbinput.container, +.yue div.nboutput.container, +.yue div.nbinput.container > div.input_area, +.yue div.nboutput.container > div.output_area { + background-color: var(--uplt-color-sidebar-bg); +}*/ + +/* Shibuya right TOC: collapse sub-H1 headings under each H1 section */ +.sy-rside .localtoc { + margin-left: 0.55rem; + border: 1px solid var(--uplt-color-border-muted); + border-radius: 0.5rem; + padding: 0.7rem 0.75rem; + background: var(--uplt-color-panel-bg); + box-shadow: 0 1px 6px var(--uplt-color-shadow); +} + +.sy-rside .sy-rside-inner > div:empty { + display: none; +} + +.sy-rside .localtoc > h3 { + color: var(--sy-c-light); + font-family: var(--sy-f-heading); + font-size: 0.86rem; + font-weight: 500; + letter-spacing: 0.4px; + text-transform: uppercase; + margin: 0 0 0.5rem 0; + padding: 0 0 0.45rem 0; + border-bottom: 1px solid var(--sy-c-divider); +} + +.sy-rside .localtoc > ul li > a { + display: block; + padding: 0.08rem 0.2rem 0.08rem 0.45rem; + border-radius: 0.2rem; +} + +.sy-rside .localtoc > ul > li.uplt-toc-collapsible { + position: relative; + padding-left: 1.2rem; +} + +.sy-rside .localtoc > .uplt-toc-controls { + display: flex; + gap: 0.35rem; + margin: 0.35rem 0 0.75rem 0; +} + +.sy-rside .localtoc > .uplt-code-controls { + display: grid; + grid-template-columns: 1fr; + row-gap: 0.35rem; + margin: 0.85rem 0 0 0; + padding-top: 0.55rem; + border-top: 1px solid var(--sy-c-divider); +} + +.sy-rside .localtoc > .uplt-code-controls .uplt-code-btn { + width: 100%; + justify-self: stretch; + text-align: left; +} + +.sy-rside .localtoc .uplt-toc-btn { + border: 1px solid var(--sy-c-border); + background: var(--uplt-color-panel-bg); + color: var(--sy-c-text); + border-radius: 6px; + padding: 0.16rem 0.5rem; + font-size: 0.73rem; + font-weight: 600; + letter-spacing: 0.01em; + line-height: 1.25; + cursor: pointer; + box-shadow: 0 1px 2px var(--uplt-color-shadow); + transition: + border-color 0.2s ease, + color 0.2s ease, + background-color 0.2s ease, + box-shadow 0.2s ease; +} + +.sy-rside .localtoc .uplt-toc-btn:hover { + border-color: var(--sy-c-link); + color: var(--sy-c-link); + background: var(--sy-c-surface); + box-shadow: 0 1px 3px var(--uplt-color-shadow); +} + +.sy-rside .localtoc .uplt-toc-btn:focus-visible { + outline: 2px solid var(--sy-c-link); + outline-offset: 1px; +} + +.uplt-rside-show { + position: fixed; + right: 1rem; + top: 5.5rem; + z-index: 50; + border: 1px solid var(--sy-c-border); + background: var(--uplt-color-panel-bg); + color: var(--sy-c-text); + border-radius: 6px; + padding: 0.24rem 0.68rem; + font-size: 0.73rem; + font-weight: 600; + cursor: pointer; + display: none; + box-shadow: 0 2px 10px var(--uplt-color-shadow); +} + +.uplt-rside-hidden .uplt-rside-show { + display: inline-flex; + align-items: center; +} + +.uplt-rside-hidden .sy-rside { + display: none; +} + +.uplt-rside-hidden .rside-overlay { + display: none; +} + +.sy-rside .localtoc > ul > li > button.uplt-toc-toggle { + position: absolute; + left: -0.1rem; + top: 0.12rem; + width: 1.2rem; + height: 1.2rem; + border-radius: 3px; + border: none; + background: transparent; + color: var(--sy-c-light); + cursor: pointer; + font-size: 0; + line-height: 1; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.sy-rside .localtoc > ul > li > button.uplt-toc-toggle::before { + content: "▸"; + font-size: 2.02rem; + transform: rotate(0deg); + transition: transform 0.2s ease; +} + +.sy-rside + .localtoc + > ul + > li + > button.uplt-toc-toggle[aria-expanded="true"]::before { + transform: rotate(90deg); +} + +.sy-rside .localtoc > ul > li > button.uplt-toc-toggle:hover { + color: var(--sy-c-link); + background: var(--sy-c-surface); +} + +.globaltoc > ul a.current, +.localtoc > ul li.active > a { + color: var(--sy-c-link) !important; +} + +.globaltoc > ul a:hover, +.localtoc > ul li > a:hover { + color: var(--sy-c-link-hover) !important; +} + +/* Left TOC: subtle colored section markers */ +.globaltoc li.toctree-l1 { + border-left: 3px solid var(--uplt-color-border-muted); + padding-left: 0.45rem; + border-radius: 0.2rem; +} + +.globaltoc li.toctree-l1:nth-of-type(6n + 1) { + border-left-color: #7fb3ad; +} + +.globaltoc li.toctree-l1:nth-of-type(6n + 2) { + border-left-color: #8fb6cc; +} + +.globaltoc li.toctree-l1:nth-of-type(6n + 3) { + border-left-color: #b4b6d8; +} + +.globaltoc li.toctree-l1:nth-of-type(6n + 4) { + border-left-color: #c0b7ce; +} + +.globaltoc li.toctree-l1:nth-of-type(6n + 5) { + border-left-color: #b7c8a7; +} + +.globaltoc li.toctree-l1:nth-of-type(6n + 6) { + border-left-color: #d2b8a4; +} + +/* API pages: increase visual separation for summary and details blocks */ +.sy-main .yue [id^="api-"] p.rubric { + margin-top: 1.25rem; + margin-bottom: 0.45rem; + padding: 0.35rem 0.6rem; + border-left: 3px solid var(--uplt-color-accent); + background: linear-gradient( + 90deg, + var(--uplt-color-accent-grad-start), + var(--uplt-color-accent-grad-end) + ); + border-radius: 0.2rem; +} + +.sy-main .yue [id^="api-"] dl.py { + margin-top: 0.85rem; + padding: 0.6rem 0.8rem; + border: 1px solid var(--uplt-color-border-muted); + border-radius: 0.35rem; + background: var(--uplt-color-panel-bg); +} + +.sy-main .yue [id^="api-"] dl.py.attribute, +.sy-main .yue [id^="api-"] dl.py.data { + border-left: 3px solid #2f7a4a; +} + +.sy-main .yue [id^="api-"] dl.py.method, +.sy-main .yue [id^="api-"] dl.py.function { + border-left: 3px solid #1f6d9c; +} + +.sy-main .yue [id^="api-"] dl.py.class, +.sy-main .yue [id^="api-"] dl.py.exception { + border-left: 3px solid #7a4a1f; +} + +.sy-main .yue [id^="api-"] dl.py dt { + padding: 0.25rem 0.35rem; + border-radius: 0.2rem; + background: var(--uplt-color-sidebar-bg); +} diff --git a/docs/_static/custom.js b/docs/_static/custom.js index bca643396..2c0106c5f 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -1,4 +1,247 @@ +function getDirectChildByTag(el, tagName) { + return ( + Array.from(el.children).find((child) => child.tagName === tagName) || null + ); +} + +function getDirectToggleButton(item) { + return ( + Array.from(item.children).find( + (child) => + child.tagName === "BUTTON" && + child.classList.contains("uplt-toc-toggle"), + ) || null + ); +} + +function setTocItemExpanded(item, expanded) { + const childList = getDirectChildByTag(item, "UL"); + const toggle = getDirectToggleButton(item); + if (!childList || !toggle) return; + childList.hidden = !expanded; + childList.style.display = expanded ? "" : "none"; + toggle.setAttribute("aria-expanded", expanded ? "true" : "false"); + toggle.classList.toggle("is-expanded", expanded); + toggle.textContent = ""; +} + +function localtocHasMeaningfulEntries(localtoc) { + const links = Array.from(localtoc.querySelectorAll("a.reference.internal")); + return links.some((link) => { + const href = (link.getAttribute("href") || "").trim(); + const text = (link.textContent || "").trim(); + return text && href && href !== "#"; + }); +} + +function getCodeDetailsBlocks() { + return Array.from(document.querySelectorAll("details.uplt-code-details")); +} + +function syncRightTocCodeButtons(localtoc) { + if (!localtoc) return; + const blocks = getCodeDetailsBlocks(); + let codeControls = + Array.from(localtoc.children).find( + (child) => + child.classList && child.classList.contains("uplt-code-controls"), + ) || null; + if (!blocks.length) { + if (codeControls) { + codeControls.remove(); + } + return; + } + + if (!codeControls) { + codeControls = document.createElement("div"); + codeControls.className = "uplt-code-controls"; + localtoc.appendChild(codeControls); + } + + let collapseCodeBtn = codeControls.querySelector(".uplt-code-collapse"); + if (!collapseCodeBtn) { + collapseCodeBtn = document.createElement("button"); + collapseCodeBtn.type = "button"; + collapseCodeBtn.className = "uplt-toc-btn uplt-code-btn uplt-code-collapse"; + collapseCodeBtn.addEventListener("click", function () { + const codeBlocks = getCodeDetailsBlocks(); + const allCollapsed = codeBlocks.length > 0 && codeBlocks.every((block) => !block.open); + if (allCollapsed) { + codeBlocks.forEach((block) => { + block.open = true; + }); + } else { + codeBlocks.forEach((block) => { + block.open = false; + }); + } + updateCodeButtonLabels(); + }); + codeControls.appendChild(collapseCodeBtn); + } + + const updateCodeButtonLabels = () => { + const codeBlocks = getCodeDetailsBlocks(); + const allCollapsed = codeBlocks.length > 0 && codeBlocks.every((block) => !block.open); + collapseCodeBtn.textContent = allCollapsed ? "Show all code" : "Collapse code"; + }; + + blocks.forEach((block) => { + if (block.dataset.upltCodeSync !== "1") { + block.addEventListener("toggle", updateCodeButtonLabels); + block.dataset.upltCodeSync = "1"; + } + }); + updateCodeButtonLabels(); +} + +function initShibuyaRightToc() { + const shibuyaRightToc = document.querySelector(".sy-rside"); + if (!shibuyaRightToc) return; + const localtoc = shibuyaRightToc.querySelector(".localtoc"); + if (!localtoc) return; + + const overlay = document.querySelector(".rside-overlay"); + if (!localtocHasMeaningfulEntries(localtoc)) { + shibuyaRightToc.style.display = "none"; + if (overlay) overlay.style.display = "none"; + return; + } + shibuyaRightToc.style.display = ""; + if (overlay) overlay.style.display = ""; + + const storageKey = "uplt.rside.hidden"; + const setRightTocHidden = (hidden) => { + document.body.classList.toggle("uplt-rside-hidden", hidden); + try { + localStorage.setItem(storageKey, hidden ? "1" : "0"); + } catch (_err) { + // Ignore storage errors in private/incognito environments. + } + }; + + if (!document.body.dataset.upltRsideStateInit) { + let restoreHidden = false; + try { + restoreHidden = localStorage.getItem(storageKey) === "1"; + } catch (_err) { + restoreHidden = false; + } + setRightTocHidden(restoreHidden); + document.body.dataset.upltRsideStateInit = "1"; + } + + let showBtn = document.querySelector(".uplt-rside-show"); + if (!showBtn) { + showBtn = document.createElement("button"); + showBtn.type = "button"; + showBtn.className = "uplt-rside-show"; + showBtn.textContent = "Show contents"; + showBtn.setAttribute("aria-label", "Show right table of contents"); + showBtn.addEventListener("click", function () { + setRightTocHidden(false); + }); + document.body.appendChild(showBtn); + } + + const topList = getDirectChildByTag(localtoc, "UL"); + if (!topList) return; + const topItems = Array.from(topList.children).filter( + (node) => node.tagName === "LI", + ); + const collapsibleItems = []; + const currentHash = (window.location.hash || "").trim(); + + topItems.forEach((item) => { + const link = + Array.from(item.children).find( + (child) => + child.tagName === "A" && + child.classList.contains("reference") && + child.classList.contains("internal"), + ) || null; + const childList = getDirectChildByTag(item, "UL"); + if (!link || !childList) return; + + item.classList.add("uplt-toc-collapsible"); + let toggle = getDirectToggleButton(item); + if (!toggle) { + toggle = document.createElement("button"); + toggle.type = "button"; + toggle.className = "uplt-toc-toggle"; + toggle.setAttribute("aria-label", "Toggle section"); + toggle.textContent = ""; + toggle.addEventListener("click", function () { + const expanded = toggle.getAttribute("aria-expanded") === "true"; + setTocItemExpanded(item, !expanded); + }); + item.insertBefore(toggle, link); + } + + const hashInChildren = + currentHash && + Array.from(childList.querySelectorAll("a.reference.internal")).some( + (a) => (a.getAttribute("href") || "").trim() === currentHash, + ); + const hashOnTop = currentHash && (link.getAttribute("href") || "") === currentHash; + if (!toggle.hasAttribute("aria-expanded")) { + setTocItemExpanded(item, !!(hashOnTop || hashInChildren)); + } else if (hashOnTop || hashInChildren) { + setTocItemExpanded(item, true); + } + + collapsibleItems.push(item); + }); + + let controls = + Array.from(localtoc.children).find( + (child) => + child.classList && child.classList.contains("uplt-toc-controls"), + ) || null; + if (!controls) { + const controls = document.createElement("div"); + controls.className = "uplt-toc-controls"; + + const collapseBtn = document.createElement("button"); + collapseBtn.type = "button"; + collapseBtn.className = "uplt-toc-btn"; + collapseBtn.textContent = "Collapse"; + collapseBtn.addEventListener("click", function () { + collapsibleItems.forEach((item) => setTocItemExpanded(item, false)); + }); + + const expandBtn = document.createElement("button"); + expandBtn.type = "button"; + expandBtn.className = "uplt-toc-btn"; + expandBtn.textContent = "Expand"; + expandBtn.addEventListener("click", function () { + collapsibleItems.forEach((item) => setTocItemExpanded(item, true)); + }); + + const hideBtn = document.createElement("button"); + hideBtn.type = "button"; + hideBtn.className = "uplt-toc-btn uplt-toc-btn-hide"; + hideBtn.textContent = "Hide"; + hideBtn.addEventListener("click", function () { + setRightTocHidden(true); + }); + + controls.appendChild(collapseBtn); + controls.appendChild(expandBtn); + controls.appendChild(hideBtn); + localtoc.insertBefore(controls, topList); + syncRightTocCodeButtons(localtoc); + return; + } + syncRightTocCodeButtons(localtoc); +} + document.addEventListener("DOMContentLoaded", function () { + // Shibuya theme: right TOC controls and collapsible sub-sections. + initShibuyaRightToc(); + window.addEventListener("hashchange", initShibuyaRightToc); + // Check if current page has opted out of the TOC if (document.body.classList.contains("no-right-toc")) { return; @@ -206,6 +449,44 @@ document.addEventListener("DOMContentLoaded", function () { }); document.addEventListener("DOMContentLoaded", function () { + const wrapWithCodeToggle = (block) => { + if (!block || !block.parentNode) return; + if (block.closest("details.uplt-code-details")) return; + const details = document.createElement("details"); + details.className = "uplt-code-details"; + const summary = document.createElement("summary"); + summary.className = "uplt-code-summary"; + summary.textContent = "Show code"; + details.appendChild(summary); + block.parentNode.insertBefore(details, block); + details.appendChild(block); + details.open = false; + details.addEventListener("toggle", function () { + summary.textContent = details.open ? "Hide code" : "Show code"; + }); + }; + + // Gallery example pages: collapse source code blocks by default. + const galleryExampleCodeBlocks = Array.from( + document.querySelectorAll( + "section.sphx-glr-example-title div.highlight-Python.notranslate", + ), + ); + galleryExampleCodeBlocks.forEach((block) => { + wrapWithCodeToggle(block); + }); + + // Notebook-style tutorial pages: collapse input code cells by default. + const notebookInputBlocks = Array.from( + document.querySelectorAll("div.nbinput.docutils.container"), + ); + notebookInputBlocks.forEach((block) => { + wrapWithCodeToggle(block); + }); + + // Re-sync right TOC controls now that code wrappers exist. + initShibuyaRightToc(); + const navLinks = document.querySelectorAll( ".wy-menu-vertical a.reference.internal", ); diff --git a/docs/conf.py b/docs/conf.py index 66f4edff6..60f77d58b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,7 +57,27 @@ def __getattr__(self, name): # Build what's news page from github releases from subprocess import run -run([sys.executable, "_scripts/fetch_releases.py"], check=False) +FAST_PREVIEW = os.environ.get("UPLT_DOCS_FAST_PREVIEW", "").strip().lower() in { + "1", + "true", + "yes", + "on", +} +if not FAST_PREVIEW: + run([sys.executable, "_scripts/fetch_releases.py"], check=False) + +# Docs theme selector. Default to Shibuya, but keep env override for A/B checks. +DOCS_THEME = os.environ.get("UPLT_DOCS_THEME", "shibuya").strip().lower() +if DOCS_THEME in {"ultratheme", "rtd", "sphinx_rtd_light_dark"}: + DOCS_THEME = "sphinx_rtd_light_dark" +else: + DOCS_THEME = "shibuya" +if DOCS_THEME == "shibuya": + try: + import shibuya # noqa: F401 + except Exception: + print("Shibuya theme not installed; falling back to sphinx_rtd_light_dark.") + DOCS_THEME = "sphinx_rtd_light_dark" # Update path for sphinx-automodapi and sphinxext extension sys.path.append(os.path.abspath(".")) @@ -194,13 +214,15 @@ def _reset_ultraplot(gallery_conf, fname): "sphinx.ext.autosummary", # autosummary directive "sphinxext.custom_roles", # local extension "sphinx_automodapi.automodapi", # fork of automodapi - "sphinx_rtd_light_dark", # use custom theme - "sphinx_sitemap", "sphinx_copybutton", # add copy button to code "_ext.notoc", "nbsphinx", # parse rst books "sphinx_gallery.gen_gallery", ] +if not FAST_PREVIEW: + extensions.append("sphinx_sitemap") +if DOCS_THEME == "sphinx_rtd_light_dark": + extensions.append("sphinx_rtd_light_dark") autosectionlabel_prefix_document = True @@ -306,6 +328,8 @@ def _reset_ultraplot(gallery_conf, fname): "pint": ("https://pint.readthedocs.io/en/stable/", None), "networkx": ("https://networkx.org/documentation/stable/", None), } +if FAST_PREVIEW: + intersphinx_mapping = {} # Fix duplicate class member documentation from autosummary + numpydoc @@ -359,7 +383,11 @@ def _reset_ultraplot(gallery_conf, fname): # Add jupytext support to nbsphinx nbsphinx_custom_formats = {".py": ["jupytext.reads", {"fmt": "py:percent"}]} -nbsphinx_execute = "auto" +# Control notebook execution from env for predictable local/CI builds. +# Use values: auto, always, never. +nbsphinx_execute = os.environ.get("UPLT_DOCS_EXECUTE", "auto").strip().lower() +if nbsphinx_execute not in {"auto", "always", "never"}: + nbsphinx_execute = "auto" # Suppress warnings in nbsphinx kernels without injecting visible cells. os.environ["PYTHONWARNINGS"] = "ignore" @@ -387,8 +415,9 @@ def _reset_ultraplot(gallery_conf, fname): } # The name of the Pygments (syntax highlighting) style to use. -# The light-dark theme toggler overloads this, but set default anyway -pygments_style = "none" +# Use non-purple-forward palettes for clearer code contrast in both modes. +pygments_style = "friendly" +pygments_dark_style = "native" html_baseurl = "https://ultraplot.readthedocs.io/stable" sitemap_url_scheme = "{link}" @@ -405,20 +434,45 @@ def _reset_ultraplot(gallery_conf, fname): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -# Use modified RTD theme with overrides in custom.css and custom.js -style = None -html_theme = "sphinx_rtd_light_dark" -# html_theme = "alabaster" -# html_theme = "sphinx_rtd_theme" -html_theme_options = { - "logo_only": True, - "collapse_navigation": True, - "navigation_depth": 4, - "prev_next_buttons_location": "bottom", # top and bottom - "includehidden": True, - "titles_only": True, - "sticky_navigation": True, -} +# Shibuya is default. Keep legacy RTD-light-dark settings for fallback builds. +if DOCS_THEME == "shibuya": + html_theme = "shibuya" + html_theme_options = { + "toctree_collapse": True, + "toctree_maxdepth": 4, + "toctree_titles_only": True, + "toctree_includehidden": True, + "globaltoc_expand_depth": 1, + "light_logo": "logo_square.png", + "dark_logo": "logo_square.png", + "logo_target": "index.html", + "accent_color": "blue", + "nav_links": [ + {"title": "Why UltraPlot?", "url": "why"}, + {"title": "Gallery", "url": "gallery/index"}, + {"title": "Installation guide", "url": "install"}, + {"title": "Usage", "url": "usage"}, + {"title": "API", "url": "api"}, + {"title": "GitHub", "url": "https://github.com/Ultraplot/UltraPlot"}, + { + "title": "Discussions", + "url": "https://github.com/Ultraplot/UltraPlot/discussions", + }, + ], + } +else: + # Use modified RTD theme with overrides in custom.css and custom.js. + style = None + html_theme = "sphinx_rtd_light_dark" + html_theme_options = { + "logo_only": True, + "collapse_navigation": True, + "navigation_depth": 4, + "prev_next_buttons_location": "bottom", # top and bottom + "includehidden": True, + "titles_only": True, + "sticky_navigation": True, + } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -447,12 +501,12 @@ def _reset_ultraplot(gallery_conf, fname): htmlhelp_basename = "ultraplotdoc" -html_css_files = [ - "custom.css", -] -html_js_files = [ - "custom.js", -] +if DOCS_THEME == "shibuya": + html_css_files = ["custom.css"] + html_js_files = ["custom.js"] +else: + html_css_files = ["custom.css"] + html_js_files = ["custom.js"] # -- Options for LaTeX output ------------------------------------------------ From e5de92e19a183a50bb17f79efa7ee26c7344980a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 13 Feb 2026 23:02:55 +1000 Subject: [PATCH 02/10] Docs theme-v2: apply dark code-surface backgrounds consistently --- docs/_static/custom.css | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index a337a959d..079669d6c 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -32,7 +32,7 @@ --uplt-color-code-bg: var(--uplt-color-sidebar-bg); /* same as page */ --uplt-color-code-fg: #6a6a6a; /* gray code text (light) */ - --code-block-background: var(--uplt-color-code-bg) !important; + --code-block-background: var(--uplt-color-code-bg); --sy-c-link: var(--uplt-color-accent); --sy-c-link-hover: #0b5f59; --uplt-color-toc-bg: #e9e9e9; @@ -216,7 +216,9 @@ html.dark-theme, --sy-c-link: #58d5c9; --sy-c-link-hover: #84e8df; --uplt-color-panel-bg: #202020; - --code-block-background: #2a2a2a; + --code-block-background: #141414; + --syntax-dark-background: #141414; + --syntax-dark-highlight: #2a2f2f; --uplt-color-toc-bg: #171717; } @@ -649,12 +651,12 @@ body.wy-body-for-nav background-color: var(--uplt-color-toc-bg); } -/*.yue div.nbinput.container, -.yue div.nboutput.container, .yue div.nbinput.container > div.input_area, -.yue div.nboutput.container > div.output_area { - background-color: var(--uplt-color-sidebar-bg); -}*/ +.yue div.nboutput.container > div.output_area, +.yue .highlight, +.yue .highlight pre { + background-color: var(--code-block-background) !important; +} /* Shibuya right TOC: collapse sub-H1 headings under each H1 section */ .sy-rside .localtoc { From c0a2f5e5668c2c2bb2f335006d85e7996e1e0081 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 13 Feb 2026 23:08:49 +1000 Subject: [PATCH 03/10] Docs theme-v2: hide right TOC on gallery listing pages --- docs/_static/custom.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/_static/custom.js b/docs/_static/custom.js index 2c0106c5f..2d97b07cb 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -99,6 +99,22 @@ function syncRightTocCodeButtons(localtoc) { function initShibuyaRightToc() { const shibuyaRightToc = document.querySelector(".sy-rside"); if (!shibuyaRightToc) return; + const path = window.location.pathname || ""; + const isGalleryIndexPage = + /\/gallery\/?$/.test(path) || + /\/gallery\/index(?:_new)?\.html$/.test(path); + const forceHideRightToc = + document.body.classList.contains("no-right-toc") || + isGalleryIndexPage || + !!document.querySelector(".sphx-glr-thumbcontainer") || + !!document.querySelector(".sphx-glr-thumbnails"); + if (forceHideRightToc) { + shibuyaRightToc.style.display = "none"; + const overlay = document.querySelector(".rside-overlay"); + if (overlay) overlay.style.display = "none"; + return; + } + const localtoc = shibuyaRightToc.querySelector(".localtoc"); if (!localtoc) return; @@ -238,6 +254,10 @@ function initShibuyaRightToc() { } document.addEventListener("DOMContentLoaded", function () { + if (document.querySelector(".sphx-glr-thumbcontainer")) { + document.body.classList.add("no-right-toc"); + } + // Shibuya theme: right TOC controls and collapsible sub-sections. initShibuyaRightToc(); window.addEventListener("hashchange", initShibuyaRightToc); From eecabe730571ab8b08fea574f21ff67701727427 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Fri, 13 Feb 2026 23:16:27 +1000 Subject: [PATCH 04/10] update theme and makefile --- docs/Makefile | 8 +++++++- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 9cd3086b6..abf9cc069 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -9,7 +9,13 @@ SPHINXPROJ = UltraPlot SOURCEDIR = . BUILDDIR = _build -.PHONY: help clean Makefile +.PHONY: help clean html html-exec Makefile + +html: + @UPLT_DOCS_EXECUTE=$${UPLT_DOCS_EXECUTE:-always} $(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" -E -a $(SPHINXOPTS) + +html-exec: + @UPLT_DOCS_EXECUTE=always $(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" -E -a $(SPHINXOPTS) # Put it first so that "make" without argument is like "make help". help: diff --git a/pyproject.toml b/pyproject.toml index 0d36747a1..c3594a083 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ docs = [ "sphinx-copybutton", "sphinx-design", "sphinx-gallery", + "shibuya", "sphinx-rtd-light-dark @ git+https://github.com/ultraplot/UltraTheme.git", "sphinx-sitemap", "typing-extensions" From 93bdea16a5cced7e165a633b8c06ae92f93b20a6 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 14 Feb 2026 06:40:47 +1000 Subject: [PATCH 05/10] Docs: wire optional UltraTheme assets and refresh homepage visual system --- docs/_static/custom.css | 138 ++++++++++++++++++++++++++++++++++++++-- docs/_static/custom.js | 51 +++++++++++++++ docs/conf.py | 20 ++++-- docs/index.rst | 22 +++---- 4 files changed, 212 insertions(+), 19 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 079669d6c..d18189098 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -72,6 +72,22 @@ padding-right: 1.25rem !important; } +.sy-head .sy-head-brand { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.2rem 0.04rem 0.2rem; + line-height: 1.25; +} + +.sy-head .sy-head-brand strong { + font-size: 0.74rem; + font-weight: 700; + letter-spacing: 0.065em; + text-transform: uppercase; + line-height: 1.25; +} + @media (min-width: 768px) { .sy-head .sy-head-links > ul { display: flex !important; @@ -121,16 +137,68 @@ opacity: 1; } +@media (min-width: 768px) { + .sy-head, + .sy-breadcrumbs, + .sy-lside { + transition: + opacity 0.24s ease, + transform 0.24s ease; + will-change: opacity, transform; + } + + html.uplt-chrome-hidden .sy-head, + html.uplt-chrome-hidden .sy-breadcrumbs { + opacity: 0; + transform: translateY(-14px); + pointer-events: none; + } + + html.uplt-chrome-hidden .sy-lside { + opacity: 0; + transform: translateX(-14px); + pointer-events: none; + } +} + /* Content heading hierarchy */ .sy-main .yue h1 { font-size: clamp(2rem, 2.6vw, 2.5rem); line-height: 1.12; - font-weight: 700; - letter-spacing: -0.01em; - margin: 0 0 1rem; + font-weight: 740; + letter-spacing: -0.018em; + margin: 0 0 1.1rem; + padding-bottom: 0.38rem; + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + column-gap: 0.7rem; + row-gap: 0.25rem; color: var(--sy-c-heading); } +.sy-main .yue h1::before { + content: ""; + grid-row: 1; + grid-column: 1; + width: 0.5rem; + height: 1.05em; + border-radius: 999px; + background: linear-gradient(180deg, var(--uplt-color-accent) 0%, #0a5f58 100%); + box-shadow: 0 0 0 1px var(--uplt-color-accent-shadow-soft); +} + +.sy-main .yue h1::after { + content: ""; + display: block; + grid-row: 2; + grid-column: 2; + width: clamp(2.8rem, 8vw, 4.2rem); + height: 0.2rem; + border-radius: 999px; + background: linear-gradient(90deg, var(--uplt-color-accent) 0%, #0a5f58 100%); +} + .sy-main .yue h2 { font-size: clamp(1.35rem, 1.8vw, 1.65rem); line-height: 1.25; @@ -225,7 +293,9 @@ html.dark-theme, @media (prefers-color-scheme: dark) { html:not(.light):not(.light-theme):not([data-color-mode="light"]) { --uplt-color-panel-bg: #202020; - --code-block-background: #2a2a2a; + --code-block-background: #141414; + --syntax-dark-background: #141414; + --syntax-dark-highlight: #2a2f2f; --uplt-color-toc-bg: #171717; } } @@ -242,6 +312,24 @@ html.dark-theme, display: flex !important; flex-direction: column !important; height: 100% !important; + border: 1px solid var(--uplt-color-border-muted) !important; + border-radius: 0.8rem !important; + background: linear-gradient( + 180deg, + var(--uplt-color-white) 0%, + var(--uplt-color-sidebar-bg) 100% + ) !important; + box-shadow: 0 4px 14px var(--uplt-color-shadow); + transition: + transform 0.18s ease, + box-shadow 0.18s ease, + border-color 0.18s ease; +} + +.card-with-bottom-text:hover { + transform: translateY(-2px); + border-color: var(--uplt-color-accent) !important; + box-shadow: 0 10px 22px var(--uplt-color-accent-shadow-soft); } /* Style the card content areas */ @@ -249,12 +337,41 @@ html.dark-theme, display: flex !important; flex-direction: column !important; flex-grow: 1 !important; + gap: 0.25rem; + padding: 0.85rem 1rem 1rem !important; +} + +.card-with-bottom-text .sd-card-header { + background: linear-gradient( + 135deg, + var(--uplt-color-accent) 0%, + #0a5f58 100% + ) !important; + color: #ffffff !important; + border-bottom: 0 !important; + border-top-left-radius: 0.8rem !important; + border-top-right-radius: 0.8rem !important; + padding: 0.72rem 1rem !important; +} + +.card-with-bottom-text .sd-card-header .sd-card-text, +.card-with-bottom-text .sd-card-header strong { + color: #ffffff !important; +} + +.card-with-bottom-text .sd-card-title { + margin-bottom: 0.35rem; + font-weight: 650; + letter-spacing: -0.01em; } /* Make images not grow or shrink */ .card-with-bottom-text img { flex-shrink: 0 !important; margin-bottom: 0.5rem !important; + border-radius: 0.45rem; + border: 1px solid var(--uplt-color-border-muted); + background: var(--uplt-color-card-bg); } /* Push the last paragraph to the bottom */ @@ -264,6 +381,19 @@ html.dark-theme, text-align: center !important; } +html.dark .card-with-bottom-text, +html.dark-theme .card-with-bottom-text, +[data-color-mode="dark"] .card-with-bottom-text { + background: linear-gradient(180deg, #252525 0%, #1f1f1f 100%) !important; + box-shadow: 0 7px 20px rgba(0, 0, 0, 0.3); +} + +html.dark .card-with-bottom-text .sd-card-header, +html.dark-theme .card-with-bottom-text .sd-card-header, +[data-color-mode="dark"] .card-with-bottom-text .sd-card-header { + background: linear-gradient(135deg, #178f84 0%, #0f6f67 100%) !important; +} + .img-container img { object-fit: cover; width: 100%; diff --git a/docs/_static/custom.js b/docs/_static/custom.js index 2d97b07cb..53cd1eeaf 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -38,6 +38,55 @@ function getCodeDetailsBlocks() { return Array.from(document.querySelectorAll("details.uplt-code-details")); } +function initScrollChromeFade() { + const topBar = document.querySelector(".sy-head"); + const leftBar = document.querySelector(".sy-lside"); + if (!topBar && !leftBar) return; + + let lastY = window.scrollY || 0; + let ticking = false; + const minDelta = 6; + const revealThreshold = 96; + + const setHidden = (hidden) => { + document.documentElement.classList.toggle("uplt-chrome-hidden", hidden); + }; + + const update = () => { + const y = window.scrollY || 0; + const delta = y - lastY; + const expanded = (document.body.getAttribute("data-expanded") || "").trim(); + const isMobileMenuOpen = + expanded.includes("head-nav") || + expanded.includes("lside") || + expanded.includes("rside"); + + if (window.innerWidth < 768 || isMobileMenuOpen || y < revealThreshold) { + setHidden(false); + } else if (delta > minDelta) { + setHidden(true); + } else if (delta < -minDelta) { + setHidden(false); + } + + lastY = y; + ticking = false; + }; + + window.addEventListener( + "scroll", + () => { + if (!ticking) { + window.requestAnimationFrame(update); + ticking = true; + } + }, + { passive: true }, + ); + window.addEventListener("resize", update, { passive: true }); + update(); +} + function syncRightTocCodeButtons(localtoc) { if (!localtoc) return; const blocks = getCodeDetailsBlocks(); @@ -254,6 +303,8 @@ function initShibuyaRightToc() { } document.addEventListener("DOMContentLoaded", function () { + initScrollChromeFade(); + if (document.querySelector(".sphx-glr-thumbcontainer")) { document.body.classList.add("no-right-toc"); } diff --git a/docs/conf.py b/docs/conf.py index 60f77d58b..5fcc100af 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -82,6 +82,16 @@ def __getattr__(self, name): # Update path for sphinx-automodapi and sphinxext extension sys.path.append(os.path.abspath(".")) sys.path.insert(0, os.path.abspath("..")) +_ultratheme_path = os.path.abspath("../UltraTheme") +if os.path.isdir(_ultratheme_path): + sys.path.insert(0, _ultratheme_path) + +try: + import ultraplot_theme # noqa: F401 + + HAVE_ULTRAPLOT_THEME_EXT = True +except Exception: + HAVE_ULTRAPLOT_THEME_EXT = False # Ensure whats_new exists during local builds without GitHub fetch. whats_new_path = Path(__file__).parent / "whats_new.rst" @@ -221,7 +231,9 @@ def _reset_ultraplot(gallery_conf, fname): ] if not FAST_PREVIEW: extensions.append("sphinx_sitemap") -if DOCS_THEME == "sphinx_rtd_light_dark": +if HAVE_ULTRAPLOT_THEME_EXT: + extensions.append("ultraplot_theme") +elif DOCS_THEME == "sphinx_rtd_light_dark": extensions.append("sphinx_rtd_light_dark") autosectionlabel_prefix_document = True @@ -501,9 +513,9 @@ def _reset_ultraplot(gallery_conf, fname): htmlhelp_basename = "ultraplotdoc" -if DOCS_THEME == "shibuya": - html_css_files = ["custom.css"] - html_js_files = ["custom.js"] +if HAVE_ULTRAPLOT_THEME_EXT: + html_css_files = [] + html_js_files = [] else: html_css_files = ["custom.css"] html_js_files = ["custom.js"] diff --git a/docs/index.rst b/docs/index.rst index 6e1b0256b..5b5ec248d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,20 +5,21 @@ **UltraPlot** is a succinct wrapper around `matplotlib `__ for creating **beautiful, publication-quality graphics** with ease. -🚀 **Key Features** | Create More, Code Less -################### -✔ **Simplified Subplot Management** – Create multi-panel plots effortlessly. +Key Features +############ +Build polished figures quickly with pragmatic defaults. +**Simplified Subplot Management** – Create multi-panel plots effortlessly. -🎨 **Smart Aesthetics** – Optimized colormaps, fonts, and styles out of the box. +**Smart Aesthetics** – Optimized colormaps, fonts, and styles out of the box. -📊 **Versatile Plot Types** – Cartesian plots, insets, colormaps, and more. +**Versatile Plot Types** – Cartesian plots, insets, colormaps, and more. -📌 **Get Started** → :doc:`Installation guide ` | :doc:`Why UltraPlot? ` | :doc:`Usage ` | :doc:`Gallery ` +**Get Started** → :doc:`Installation guide ` | :doc:`Why UltraPlot? ` | :doc:`Usage ` | :doc:`Gallery ` -------------------------------------- -**📖 User Guide** -################# +User Guide +########## A preview of what UltraPlot can do. For more see the sidebar! .. grid:: 1 2 3 3 @@ -105,9 +106,8 @@ A preview of what UltraPlot can do. For more see the sidebar! Use prebuilt colormaps and define your own color cycles. - -**📚 Reference & More** -####################### +Reference & More +################ For more details, check the full :doc:`User guide ` and :doc:`API Reference `. * :ref:`genindex` From f4940baa1f5059bf1937f2cb0020f07034deeb0d Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 14 Feb 2026 06:51:34 +1000 Subject: [PATCH 06/10] Docs TOC UX: hide collapse controls when unused and move Hide action to title row --- docs/_static/custom.css | 28 +++++++++++++++++- docs/_static/custom.js | 65 +++++++++++++++++++++++++++++------------ 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index d18189098..000c2a324 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -814,6 +814,28 @@ body.wy-body-for-nav border-bottom: 1px solid var(--sy-c-divider); } +.sy-rside .localtoc > .uplt-toc-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.45rem; + margin: 0 0 0.5rem 0; + padding: 0 0 0.45rem 0; + border-bottom: 1px solid var(--sy-c-divider); +} + +.sy-rside .localtoc > .uplt-toc-head > h3 { + color: var(--sy-c-light); + font-family: var(--sy-f-heading); + font-size: 0.86rem; + font-weight: 500; + letter-spacing: 0.4px; + text-transform: uppercase; + margin: 0; + padding: 0; + border: 0; +} + .sy-rside .localtoc > ul li > a { display: block; padding: 0.08rem 0.2rem 0.08rem 0.45rem; @@ -828,7 +850,7 @@ body.wy-body-for-nav .sy-rside .localtoc > .uplt-toc-controls { display: flex; gap: 0.35rem; - margin: 0.35rem 0 0.75rem 0; + margin: 0 0 0.75rem 0; } .sy-rside .localtoc > .uplt-code-controls { @@ -865,6 +887,10 @@ body.wy-body-for-nav box-shadow 0.2s ease; } +.sy-rside .localtoc > .uplt-toc-head .uplt-toc-btn-hide { + padding: 0.14rem 0.42rem; +} + .sy-rside .localtoc .uplt-toc-btn:hover { border-color: var(--sy-c-link); color: var(--sy-c-link); diff --git a/docs/_static/custom.js b/docs/_static/custom.js index 53cd1eeaf..4bd28d2d2 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -212,6 +212,32 @@ function initShibuyaRightToc() { const topList = getDirectChildByTag(localtoc, "UL"); if (!topList) return; + + let headRow = + Array.from(localtoc.children).find( + (child) => child.classList && child.classList.contains("uplt-toc-head"), + ) || null; + const directHeading = getDirectChildByTag(localtoc, "H3"); + if (!headRow && directHeading) { + headRow = document.createElement("div"); + headRow.className = "uplt-toc-head"; + localtoc.insertBefore(headRow, directHeading); + headRow.appendChild(directHeading); + } + if (headRow) { + let hideBtn = headRow.querySelector(".uplt-toc-btn-hide"); + if (!hideBtn) { + hideBtn = document.createElement("button"); + hideBtn.type = "button"; + hideBtn.className = "uplt-toc-btn uplt-toc-btn-hide"; + hideBtn.textContent = "Hide"; + hideBtn.addEventListener("click", function () { + setRightTocHidden(true); + }); + headRow.appendChild(hideBtn); + } + } + const topItems = Array.from(topList.children).filter( (node) => node.tagName === "LI", ); @@ -264,41 +290,42 @@ function initShibuyaRightToc() { (child) => child.classList && child.classList.contains("uplt-toc-controls"), ) || null; + if (!collapsibleItems.length) { + if (controls) controls.remove(); + syncRightTocCodeButtons(localtoc); + return; + } + if (!controls) { - const controls = document.createElement("div"); + controls = document.createElement("div"); controls.className = "uplt-toc-controls"; + localtoc.insertBefore(controls, topList); + } - const collapseBtn = document.createElement("button"); + let collapseBtn = controls.querySelector(".uplt-toc-btn-collapse"); + if (!collapseBtn) { + collapseBtn = document.createElement("button"); collapseBtn.type = "button"; - collapseBtn.className = "uplt-toc-btn"; + collapseBtn.className = "uplt-toc-btn uplt-toc-btn-collapse"; collapseBtn.textContent = "Collapse"; collapseBtn.addEventListener("click", function () { collapsibleItems.forEach((item) => setTocItemExpanded(item, false)); }); + controls.appendChild(collapseBtn); + } - const expandBtn = document.createElement("button"); + let expandBtn = controls.querySelector(".uplt-toc-btn-expand"); + if (!expandBtn) { + expandBtn = document.createElement("button"); expandBtn.type = "button"; - expandBtn.className = "uplt-toc-btn"; + expandBtn.className = "uplt-toc-btn uplt-toc-btn-expand"; expandBtn.textContent = "Expand"; expandBtn.addEventListener("click", function () { collapsibleItems.forEach((item) => setTocItemExpanded(item, true)); }); - - const hideBtn = document.createElement("button"); - hideBtn.type = "button"; - hideBtn.className = "uplt-toc-btn uplt-toc-btn-hide"; - hideBtn.textContent = "Hide"; - hideBtn.addEventListener("click", function () { - setRightTocHidden(true); - }); - - controls.appendChild(collapseBtn); controls.appendChild(expandBtn); - controls.appendChild(hideBtn); - localtoc.insertBefore(controls, topList); - syncRightTocCodeButtons(localtoc); - return; } + syncRightTocCodeButtons(localtoc); } From e9877b584f6a3ec264346be868c5a9e56ed43201 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 14 Feb 2026 21:30:04 +1000 Subject: [PATCH 07/10] Numerous fixes across different docs and part of the api --- docs/1dplots.py | 31 ++++--- docs/2dplots.py | 38 ++++---- docs/subplots.py | 23 +++-- ultraplot/axes/base.py | 183 ++++++++++++++++++--------------------- ultraplot/constructor.py | 2 +- 5 files changed, 141 insertions(+), 136 deletions(-) diff --git a/docs/1dplots.py b/docs/1dplots.py index 47cc1a820..7c4ba9ae3 100644 --- a/docs/1dplots.py +++ b/docs/1dplots.py @@ -55,7 +55,7 @@ # # By default, when choosing the *x* or *y* axis limits, # UltraPlot ignores out-of-bounds data along the other axis if it was explicitly -# fixed by :func:`~matplotlib.axes.Axes.set_xlim` or :func:`~matplotlib.axes.Axes.set_ylim` (or, +# fixed by :py:meth:`~matplotlib.axes.Axes.set_xlim` or :py:meth:`~matplotlib.axes.Axes.set_ylim` (or, # equivalently, by passing `xlim` or `ylim` to :func:`ultraplot.axes.CartesianAxes.format`). # This can be useful if you wish to restrict the view along a "dependent" variable # axis within a large dataset. To disable this feature, pass ``inbounds=False`` to @@ -63,9 +63,10 @@ # the :rcraw:`cmap.inbounds` setting and the :ref:`user guide `). # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + N = 5 state = np.random.RandomState(51423) with uplt.rc.context({"axes.prop_cycle": uplt.Cycle("Grays", N=N, left=0.3)}): @@ -92,9 +93,10 @@ fig.format(xlabel="xlabel", ylabel="ylabel") # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + # Sample data cycle = uplt.Cycle("davos", right=0.8) state = np.random.RandomState(51423) @@ -161,9 +163,9 @@ # :func:`~pint.UnitRegistry.setup_matplotlib` so that the axes become unit-aware. # %% -import xarray as xr import numpy as np import pandas as pd +import xarray as xr # DataArray state = np.random.RandomState(51423) @@ -230,9 +232,10 @@ # `__. # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + # Sample data M, N = 50, 5 state = np.random.RandomState(51423) @@ -282,9 +285,10 @@ # "positive" lines using ``negpos=True`` (see :ref:`below ` for details). # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + state = np.random.RandomState(51423) gs = uplt.GridSpec(nrows=3, ncols=2) fig = uplt.figure(refwidth=2.2, span=False, share="labels") @@ -358,10 +362,11 @@ # calls :func:`~ultraplot.axes.PlotAxes.scatter` internally. # %% -import ultraplot as uplt import numpy as np import pandas as pd +import ultraplot as uplt + # Sample data state = np.random.RandomState(51423) x = (state.rand(20) - 0).cumsum() @@ -421,10 +426,11 @@ # plot with a colorbar indicating the parametric coordinate. # %% -import ultraplot as uplt import numpy as np import pandas as pd +import ultraplot as uplt + gs = uplt.GridSpec(ncols=2, wratios=(2, 1)) fig = uplt.figure(figwidth="16cm", refaspect=(2, 1), share=False) fig.format(suptitle="Parametric plots demo") @@ -516,10 +522,11 @@ # :func:`~ultraplot.axes.PlotAxes.bar` or :func:`~ultraplot.axes.PlotAxes.barh` internally. # %% -import ultraplot as uplt import numpy as np import pandas as pd +import ultraplot as uplt + # Sample data state = np.random.RandomState(51423) data = state.rand(5, 5).cumsum(axis=0).cumsum(axis=1)[:, ::-1] @@ -555,9 +562,10 @@ uplt.rc.reset() # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + # Sample data state = np.random.RandomState(51423) data = state.rand(5, 3).cumsum(axis=0) @@ -611,9 +619,10 @@ # ``negcolor=color`` and ``poscolor=color`` to the :class:`~ultraplot.axes.PlotAxes` commands. # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + # Sample data state = np.random.RandomState(51423) data = 4 * (state.rand(40) - 0.5) diff --git a/docs/2dplots.py b/docs/2dplots.py index fe1d4ef56..15a1ef5d6 100644 --- a/docs/2dplots.py +++ b/docs/2dplots.py @@ -58,7 +58,7 @@ # direction is automatically reversed. If coordinate *centers* are passed to commands # like :func:`~ultraplot.axes.PlotAxes.pcolor` and :func:`~ultraplot.axes.PlotAxes.pcolormesh`, they # are automatically converted to edges using :func:`~ultraplot.utils.edges` or -# `:func:`~ultraplot.utils.edges2d``, and if coordinate *edges* are passed to commands like +# :func:`~ultraplot.utils.edges2d`, and if coordinate *edges* are passed to commands like # :func:`~ultraplot.axes.PlotAxes.contour` and :func:`~ultraplot.axes.PlotAxes.contourf`, they are # automatically converted to centers (notice the locations of the rectangle edges # in the ``pcolor`` plots below). All positional arguments can also be specified @@ -161,11 +161,11 @@ # # The 2D :class:`~ultraplot.axes.PlotAxes` commands recognize `pandas`_ # and `xarray`_ data structures. If you omit *x* and *y* coordinates, -# the commands try to infer them from the `pandas.DataFrame` or -# `xarray.DataArray`. If you did not explicitly set the *x* or *y* axis label +# the commands try to infer them from the :class:`pandas.DataFrame` or +# :class:`xarray.DataArray`. If you did not explicitly set the *x* or *y* axis label # or :ref:`legend or colorbar ` label(s), the commands -# try to retrieve them from the `pandas.DataFrame` or `xarray.DataArray`. -# The commands also recognize `pint.Quantity` structures and apply +# try to retrieve them from the :class:`~pandas.DataFrame` or :class:`~xarray.DataArray`. +# The commands also recognize :class:`~pint.Quantity` structures and apply # unit string labels with formatting specified by :rc:`unitformat`. # # These features restore some of the convenience you get with the builtin @@ -176,14 +176,14 @@ # # .. note:: # -# For every plotting command, you can pass a `~xarray.Dataset`, :class:`~pandas.DataFrame`, +# For every plotting command, you can pass a :class:`~xarray.Dataset`, :class:`~pandas.DataFrame`, # or `dict` to the `data` keyword with strings as data arguments instead of arrays # -- just like matplotlib. For example, ``ax.plot('y', data=dataset)`` and # ``ax.plot(y='y', data=dataset)`` are translated to ``ax.plot(dataset['y'])``. # This is the preferred input style for most `seaborn`_ plotting commands. -# Also, if you pass a `pint.Quantity` or :class:`~xarray.DataArray` -# containing a `pint.Quantity`, UltraPlot will automatically call -# `~pint.UnitRegistry.setup_matplotlib` so that the axes become unit-aware. +# Also, if you pass a :class:`pint.Quantity` or :py:class:`~xarray.DataArray` +# containing a :class:`~pint.Quantity`, UltraPlot will automatically call +# :py:meth:`~pint.UnitRegistry.setup_matplotlib` so that the axes become unit-aware. # %% import numpy as np @@ -356,13 +356,13 @@ # ------------------- # # UltraPlot includes two new :ref:`"continuous" normalizers `. The -# `~ultraplot.colors.SegmentedNorm` normalizer provides even color gradations with respect +# :class:`~ultraplot.colors.SegmentedNorm` normalizer provides even color gradations with respect # to index for an arbitrary monotonically increasing or decreasing list of levels. This # is automatically applied if you pass unevenly spaced `levels` to a plotting command, # or it can be manually applied using e.g. ``norm='segmented'``. This can be useful for # datasets with unusual statistical distributions or spanning many orders of magnitudes. # -# The `~ultraplot.colors.DivergingNorm` normalizer ensures that colormap midpoints lie +# The :class:`~ultraplot.colors.DivergingNorm` normalizer ensures that colormap midpoints lie # on some central data value (usually ``0``), even if `vmin`, `vmax`, or `levels` # are asymmetric with respect to the central value. This is automatically applied # if your data contains negative and positive values (see :ref:`below `), @@ -440,14 +440,14 @@ # Discrete levels # --------------- # -# By default, UltraPlot uses `~ultraplot.colors.DiscreteNorm` to "discretize" +# By default, UltraPlot uses :class:`~ultraplot.colors.DiscreteNorm` to "discretize" # the possible colormap colors for contour and pseudocolor :class:`~ultraplot.axes.PlotAxes` # commands (e.g., :func:`~ultraplot.axes.PlotAxes.contourf`, :func:`~ultraplot.axes.PlotAxes.pcolor`). -# This is analogous to `matplotlib.colors.BoundaryNorm`, except -# `~ultraplot.colors.DiscreteNorm` can be paired with arbitrary +# This is analogous to :class:`matplotlib.colors.BoundaryNorm`, except +# :class:`~ultraplot.colors.DiscreteNorm` can be paired with arbitrary # continuous normalizers specified by `norm` (see :ref:`above `). # Discrete color levels can help readers discern exact numeric values and -# tend to reveal qualitative structure in the data. `~ultraplot.colors.DiscreteNorm` +# tend to reveal qualitative structure in the data. :class:`~ultraplot.colors.DiscreteNorm` # also repairs the colormap end-colors by ensuring the following conditions are met: # # #. All colormaps always span the *entire color range* @@ -458,7 +458,7 @@ # To explicitly toggle discrete levels on or off, change :rcraw:`cmap.discrete` # or pass ``discrete=False`` or ``discrete=True`` to any plotting command # that accepts a `cmap` argument. The level edges or centers used with -# `~ultraplot.colors.DiscreteNorm` can be explicitly specified using the `levels` or +# :class:`~ultraplot.colors.DiscreteNorm` can be explicitly specified using the `levels` or # `values` keywords, respectively (:func:`~ultraplot.utils.arange` and :func:`~ultraplot.utils.edges` # are useful for generating `levels` and `values` lists). You can also pass an integer # to these keywords (or to the `N` keyword) to automatically generate approximately this @@ -560,7 +560,7 @@ # UltraPlot can automatically detect "diverging" datasets. By default, # the 2D :class:`~ultraplot.axes.PlotAxes` commands will apply the diverging colormap # :rc:`cmap.diverging` (rather than :rc:`cmap.sequential`) and the diverging -# normalizer `~ultraplot.colors.DivergingNorm` (rather than :class:`~matplotlib.colors.Normalize` +# normalizer :class:`~ultraplot.colors.DivergingNorm` (rather than :class:`~matplotlib.colors.Normalize` # -- see :ref:`above `) if the following conditions are met: # # #. If discrete levels are enabled (see :ref:`above `) and the @@ -613,7 +613,7 @@ # plots by passing ``labels=True`` to the plotting command. The # label text is colored black or white depending on the luminance of the underlying # grid box or filled contour (see the section on :ref:`colorspaces `). -# Contour labels are drawn with `~matplotlib.axes.Axes.clabel` and grid box +# Contour labels are drawn with :meth:`~matplotlib.axes.Axes.clabel` and grid box # labels are drawn with :func:`~ultraplot.axes.Axes.text`. You can pass keyword arguments # to these functions by passing a dictionary to `labels_kw`, and you can # change the label precision using the `precision` keyword. See the plotting @@ -676,7 +676,7 @@ # gridlines, no minor ticks, and major ticks at the center of each box. Among other # things, this is useful for displaying covariance and correlation matrices, as shown # below. :func:`~ultraplot.axes.PlotAxes.heatmap` should generally only be used with -# `~ultraplot.axes.CartesianAxes`. +# :class:`~ultraplot.axes.CartesianAxes`. # %% import numpy as np diff --git a/docs/subplots.py b/docs/subplots.py index a1b309ec9..8845ba5d1 100644 --- a/docs/subplots.py +++ b/docs/subplots.py @@ -181,9 +181,10 @@ # depending on the number of subplots in the figure. # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + # Grid of images (note the square pixels) state = np.random.RandomState(51423) colors = np.tile(state.rand(8, 12, 1), (1, 1, 3)) @@ -353,7 +354,7 @@ # ------------------ # # Figures with lots of subplots often have :ref:`redundant labels `. -# To help address this, the matplotlib command `matplotlib.pyplot.subplots` includes +# To help address this, the matplotlib command :py:func:`matplotlib.pyplot.subplots` includes # `sharex` and `sharey` keywords that permit sharing axis limits and ticks between # like rows and columns of subplots. UltraPlot builds on this feature by: # @@ -370,7 +371,7 @@ # It is controlled by the `spanx` and `spany` :class:`~ultraplot.figure.Figure` # keywords (default is :rc:`subplots.span`). Use the `span` keyword # as a shorthand to set both `spanx` and `spany`. Note that unlike -# `~matplotlib.figure.Figure.supxlabel` and `~matplotlib.figure.Figure.supylabel`, +# :py:func:`~matplotlib.figure.Figure.supxlabel` and :py:func:`~matplotlib.figure.Figure.supylabel`, # these labels are aligned between gridspec edges rather than figure edges. # #. Supporting five sharing "levels". These values can be passed to `sharex`, # `sharey`, or `share`, or assigned to :rcraw:`subplots.share`. @@ -398,9 +399,10 @@ # settings on the appearance of several subplot grids. # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + N = 50 M = 40 state = np.random.RandomState(51423) @@ -428,9 +430,10 @@ ) # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + # The default `share='auto'` keeps incompatible axis families unshared. fig, axs = uplt.subplots(ncols=2, proj=("cart", "polar")) x = np.linspace(0, 2 * np.pi, 100) @@ -442,9 +445,10 @@ ) # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + state = np.random.RandomState(51423) # Plots with minimum and maximum sharing settings @@ -475,7 +479,9 @@ # complex layouts, UltraPlot will add the labels when the subplot # is facing and "edge" which is defined as not immediately having a subplot next to it. For example: # %% -import ultraplot as uplt, numpy as np +import numpy as np + +import ultraplot as uplt layout = [[1, 0, 2], [0, 3, 0], [4, 0, 6]] fig, ax = uplt.subplots(layout) @@ -522,9 +528,10 @@ # and `points `__. # %% -import ultraplot as uplt import numpy as np +import ultraplot as uplt + with uplt.rc.context(fontsize="12px"): # depends on rc['figure.dpi'] fig, axs = uplt.subplots( ncols=3, diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 4ec1ac1f8..ae8086a76 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -97,7 +97,7 @@ # Projection docstring _proj_docstring = """ -proj, projection : \\ +proj, projection : str, `cartopy.crs.Projection`, or `~mpl_toolkits.basemap.Basemap`, optional The map projection specification(s). If ``'cart'`` or ``'cartesian'`` (the default), a `~ultraplot.axes.CartesianAxes` is created. If ``'polar'``, @@ -160,13 +160,12 @@ # Transform docstring # Used for text and add_axes _transform_docstring = """ -transform : {'data', 'axes', 'figure', 'subfigure'} \\ -or `~matplotlib.transforms.Transform`, optional +transform : {'data', 'axes', 'figure', 'subfigure'} or `~matplotlib.transforms.Transform`, optional The transform used to interpret the bounds. Can be a - `~matplotlib.transforms.Transform` instance or a string representing - the `~matplotlib.axes.Axes.transData`, `~matplotlib.axes.Axes.transAxes`, - `~matplotlib.figure.Figure.transFigure`, or - `~matplotlib.figure.Figure.transSubfigure`, transforms. + :class:`~matplotlib.transforms.Transform` instance or a string representing + the :class:`~matplotlib.axes.Axes.transData`, :class:`~matplotlib.axes.Axes.transAxes`, + :class:`~matplotlib.figure.Figure.transFigure`, or + :class:`~matplotlib.figure.Figure.transSubfigure`, transforms. """ docstring._snippet_manager["axes.transform"] = _transform_docstring @@ -372,7 +371,7 @@ abctitlepad : float, default: :rc:`abc.titlepad` The horizontal padding between a-b-c labels and titles in the same location. %(units.pt)s -ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle : str or sequence, optional \\ +ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle : str or sequence, optional Shorthands for the below keywords. lefttitle, centertitle, righttitle, upperlefttitle, uppercentertitle, upperrighttitle : str or sequence, optional lowerlefttitle, lowercentertitle, lowerrighttitle : str or sequence, optional @@ -392,7 +391,7 @@ Labels for the subplots lying along the left, top, right, and bottom edges of the figure. The length of each list must match the number of subplots along the corresponding edge. -leftlabelpad, toplabelpad, rightlabelpad, bottomlabelpad : float or unit-spec, default\\ +leftlabelpad, toplabelpad, rightlabelpad, bottomlabelpad : float or unit-spec, default : :rc:`leftlabel.pad`, :rc:`toplabel.pad`, :rc:`rightlabel.pad`, :rc:`bottomlabel.pad` The padding between the labels and the axes content. %(units.pt)s @@ -440,37 +439,37 @@ # Colorbar docstrings _colorbar_args_docstring = """ -mappable : mappable, colormap-spec, sequence of color-spec, \\ -or sequence of `~matplotlib.artist.Artist` - There are four options here: - - 1. A `~matplotlib.cm.ScalarMappable` (e.g., an object returned by - `~ultraplot.axes.PlotAxes.contourf` or `~ultraplot.axes.PlotAxes.pcolormesh`). - 2. A `~matplotlib.colors.Colormap` or registered colormap name used to build a - `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar range and ticks depend - on the arguments `values`, `vmin`, `vmax`, and `norm`. The default for a - :class:`~ultraplot.colors.ContinuousColormap` is ``vmin=0`` and ``vmax=1`` (note that - passing `values` will "discretize" the colormap). The default for a - :class:`~ultraplot.colors.DiscreteColormap` is ``values=np.arange(0, cmap.N)``. - 3. A sequence of hex strings, color names, or RGB[A] tuples. A - :class:`~ultraplot.colors.DiscreteColormap` will be generated from these colors and - used to build a `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar - range and ticks depend on the arguments `values`, `norm`, and - `norm_kw`. The default is ``values=np.arange(0, len(mappable))``. - 4. A sequence of `matplotlib.artist.Artist` instances (e.g., a list of - `~matplotlib.lines.Line2D` instances returned by `~ultraplot.axes.PlotAxes.plot`). - A colormap will be generated from the colors of these objects (where the - color is determined by ``get_color``, if available, or ``get_facecolor``). - The colorbar range and ticks depend on the arguments `values`, `norm`, and - `norm_kw`. The default is to infer colorbar ticks and tick labels - by calling `~matplotlib.artist.Artist.get_label` on each artist. - -values : sequence of float or str, optional - Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. This maps the colormap - colors to numeric values using `~ultraplot.colors.DiscreteNorm`. If the colormap is - a :class:`~ultraplot.colors.ContinuousColormap` then its colors will be "discretized". - These These can also be strings, in which case the list indices are used for - tick locations and the strings are applied as tick labels. + mappable : mappable, colormap-spec, sequence of color-spec, + or sequence of :class:`~matplotlib.artist.Artist` + There are four options here: + + 1. A `~matplotlib.cm.ScalarMappable` (e.g., an object returned by + `~ultraplot.axes.PlotAxes.contourf` or `~ultraplot.axes.PlotAxes.pcolormesh`). + 2. A `~matplotlib.colors.Colormap` or registered colormap name used to build a + `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar range and ticks depend + on the arguments `values`, `vmin`, `vmax`, and `norm`. The default for a + :class:`~ultraplot.colors.ContinuousColormap` is ``vmin=0`` and ``vmax=1`` (note that + passing `values` will "discretize" the colormap). The default for a + :class:`~ultraplot.colors.DiscreteColormap` is ``values=np.arange(0, cmap.N)``. + 3. A sequence of hex strings, color names, or RGB[A] tuples. A + :class:`~ultraplot.colors.DiscreteColormap` will be generated from these colors and + used to build a `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar + range and ticks depend on the arguments `values`, `norm`, and + `norm_kw`. The default is ``values=np.arange(0, len(mappable))``. + 4. A sequence of `matplotlib.artist.Artist` instances (e.g., a list of + `~matplotlib.lines.Line2D` instances returned by `~ultraplot.axes.PlotAxes.plot`). + A colormap will be generated from the colors of these objects (where the + color is determined by ``get_color``, if available, or ``get_facecolor``). + The colorbar range and ticks depend on the arguments `values`, `norm`, and + `norm_kw`. The default is to infer colorbar ticks and tick labels + by calling `~matplotlib.artist.Artist.get_label` on each artist. + + values : sequence of float or str, optional + Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. This maps the colormap + colors to numeric values using `~ultraplot.colors.DiscreteNorm`. If the colormap is + a :class:`~ultraplot.colors.ContinuousColormap` then its colors will be "discretized". + These These can also be strings, in which case the list indices are used for + tick locations and the strings are applied as tick labels. """ _colorbar_kwargs_docstring = """ orientation : {None, 'horizontal', 'vertical'}, optional @@ -548,17 +547,14 @@ or :rc:`tick.width` if `linewidth` was not passed. tickwidthratio : float, default: :rc:`tick.widthratio` Relative scaling of `tickwidth` used to determine minor tick widths. -ticklabelcolor, ticklabelsize, ticklabelweight \\ -: default: :rc:`tick.labelcolor`, :rc:`tick.labelsize`, :rc:`tick.labelweight`. +ticklabelcolor, ticklabelsize, ticklabelweight: default: :rc:`tick.labelcolor`, :rc:`tick.labelsize`, :rc:`tick.labelweight`. The font color, size, and weight for colorbar tick labels labelloc, labellocation : {'bottom', 'top', 'left', 'right'} The colorbar label location. Inherits from `tickloc` by default. Default is toward the outside of the subplot for outer colorbars and ``'bottom'`` for inset colorbars. -labelcolor, labelsize, labelweight \\ -: default: :rc:`label.color`, :rc:`label.size`, and :rc:`label.weight`. +labelcolor, labelsize, labelweight: default: :rc:`label.color`, :rc:`label.size`, and :rc:`label.weight`. The font color, size, and weight for the colorbar label. -a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth : default\\ -: :rc:`colorbar.framealpha`, :rc:`colorbar.framecolor` +a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth : default: :rc:`colorbar.framealpha`, :rc:`colorbar.framecolor` For inset colorbars only. Controls the transparency and color of the background frame. lw, linewidth, c, color : optional @@ -614,7 +610,7 @@ from the artists in the tuple (if there are multiple unique labels in the tuple group of artists, the tuple group is expanded into unique legend entries -- otherwise, the tuple group elements are drawn on top of eachother). For details - on matplotlib legend handlers and tuple groups, see the matplotlib `legend guide \\ + on matplotlib legend handlers and tuple groups, see the matplotlib `legend guide -`__. """ _legend_kwargs_docstring = """ @@ -645,14 +641,10 @@ titlefontsize, titlefontweight, titlefontcolor : optional The font size, weight, and color for the legend title. Font size is interpreted by `~ultraplot.utils.units`. The default size is `fontsize`. -borderpad, borderaxespad, handlelength, handleheight, handletextpad, \\ -labelspacing, columnspacing : unit-spec, optional +borderpad, borderaxespad, handlelength, handleheight, handletextpad, labelspacing, columnspacing : unit-spec, optional Various matplotlib `~matplotlib.axes.Axes.legend` spacing arguments. %(units.em)s -a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth \\ -: default: :rc:`legend.framealpha`, :rc:`legend.facecolor`, :rc:`legend.edgecolor`, \\ -:rc:`axes.linewidth` - The opacity, face color, edge color, and edge width for the legend frame. +a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth: default: :rc:`legend.framealpha`, :rc:`legend.facecolor`, :rc:`legend.edgecolor`, :rc:`axes.linewidth` The opacity, face color, edge color, and edge width for the legend frame. c, color, lw, linewidth, m, marker, ls, linestyle, dashes, ms, markersize : optional Properties used to override the legend handles. For example, for a legend describing variations in line style ignoring variations @@ -3685,46 +3677,44 @@ def colorbar(self, mappable, values=None, loc=None, location=None, **kwargs): Parameters ---------- %(axes.colorbar_args)s - loc, location : int or str, default: :rc:`colorbar.loc` - The colorbar location. Valid location keys are shown in the below table. - - .. _colorbar_table: - - ================== ======================================= - Location Valid keys - ================== ======================================= - outer left ``'left'``, ``'l'`` - outer right ``'right'``, ``'r'`` - outer bottom ``'bottom'``, ``'b'`` - outer top ``'top'``, ``'t'`` - default inset ``'best'``, ``'inset'``, ``'i'``, ``0`` - upper right inset ``'upper right'``, ``'ur'``, ``1`` - upper left inset ``'upper left'``, ``'ul'``, ``2`` - lower left inset ``'lower left'``, ``'ll'``, ``3`` - lower right inset ``'lower right'``, ``'lr'``, ``4`` - "filled" ``'fill'`` - ================== ======================================= - - shrink - Alias for `length`. This is included for consistency with - `matplotlib.figure.Figure.colorbar`. - length \\ -: float or unit-spec, default: :rc:`colorbar.length` or :rc:`colorbar.insetlength` - The colorbar length. For outer colorbars, units are relative to the axes - width or height (default is :rcraw:`colorbar.length`). For inset - colorbars, floats interpreted as em-widths and strings interpreted - by `~ultraplot.utils.units` (default is :rcraw:`colorbar.insetlength`). - width : unit-spec, default: :rc:`colorbar.width` or :rc:`colorbar.insetwidth` - The colorbar width. For outer colorbars, floats are interpreted as inches - (default is :rcraw:`colorbar.width`). For inset colorbars, floats are - interpreted as em-widths (default is :rcraw:`colorbar.insetwidth`). - Strings are interpreted by `~ultraplot.utils.units`. - %(axes.colorbar_space)s - Has no visible effect if `length` is ``1``. - - Other parameters - ---------------- - %(axes.colorbar_kwargs)s + loc, location : int or str, default: :rc:`colorbar.loc` + The colorbar location. Valid location keys are shown in the below table. + + .. _colorbar_table: + + ================== ======================================= + Location Valid keys + ================== ======================================= + outer left ``'left'``, ``'l'`` + outer right ``'right'``, ``'r'`` + outer bottom ``'bottom'``, ``'b'`` + outer top ``'top'``, ``'t'`` + default inset ``'best'``, ``'inset'``, ``'i'``, ``0`` + upper right inset ``'upper right'``, ``'ur'``, ``1`` + upper left inset ``'upper left'``, ``'ul'``, ``2`` + lower left inset ``'lower left'``, ``'ll'``, ``3`` + lower right inset ``'lower right'``, ``'lr'``, ``4`` + "filled" ``'fill'`` + ================== ======================================= + + shrink + Alias for `length`. This is included for consistency with + `matplotlib.figure.Figure.colorbar`. + length : float or unit-spec, default: :rc:`colorbar.length` or :rc:`colorbar.insetlength` + The colorbar length. For outer colorbars, units are relative to the axes + width or height (default is :rcraw:`colorbar.length`). For inset + colorbars, floats interpreted as em-widths and strings interpreted + by `~ultraplot.utils.units` (default is :rcraw:`colorbar.insetlength`). + width : unit-spec, default: :rc:`colorbar.width` or :rc:`colorbar.insetwidth` + The colorbar width. For outer colorbars, floats are interpreted as inches + (default is :rcraw:`colorbar.width`). For inset colorbars, floats are + interpreted as em-widths (default is :rcraw:`colorbar.insetwidth`). + Strings are interpreted by `~ultraplot.utils.units`. + %(axes.colorbar_space)s + Has no visible effect if `length` is ``1``. + Other parameters + ---------------- + %(axes.colorbar_kwargs)s See also -------- @@ -3951,8 +3941,7 @@ def text( borderinvert : bool, optional If ``True``, the text and border colors are swapped. borderstyle : {'miter', 'round', 'bevel'}, default: :rc:`text.borderstyle` - The `line join style \\ -`__ + The `line join style `__ used for the border. bbox : bool, default: False Whether to draw a bounding box around text. @@ -4144,6 +4133,7 @@ def annotate( obj._annotation = ann return obj + @docstring._snippet_manager def curvedtext( self, x, @@ -4202,8 +4192,7 @@ def curvedtext( min_advance : float, default: :rc:`text.curved.min_advance` Minimum additional spacing (pixels) enforced between glyph centers. borderstyle : {'miter', 'round', 'bevel'}, default: 'miter' - The `line join style \\ -`__ + The `line join style `__ used for the border. bbox : bool, default: False Whether to draw a bounding box around text. diff --git a/ultraplot/constructor.py b/ultraplot/constructor.py index dfa39da2e..2dc83f28e 100644 --- a/ultraplot/constructor.py +++ b/ultraplot/constructor.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -T"he constructor functions used to build class instances from simple shorthand arguments. +The constructor functions used to build class instances from simple shorthand arguments. """ # NOTE: These functions used to be in separate files like crs.py and From b9e6421dc46e529e09ce88aaa1ef1bb2ab062482 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 14 Feb 2026 21:46:15 +1000 Subject: [PATCH 08/10] Make plots blend with background --- docs/_static/custom.css | 12 +++++++++++- docs/conf.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 000c2a324..f1fcaf962 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -24,6 +24,8 @@ --uplt-color-accent-grad-end: rgba(15, 118, 110, 0.02); --uplt-color-accent-shadow-strong: rgba(15, 118, 110, 0.2); --uplt-color-accent-shadow-soft: rgba(15, 118, 110, 0.1); + --uplt-color-plot-panel-bg: #f2f4f6; + --uplt-color-plot-panel-border: #d9dde2; /* Scrollbar */ --uplt-color-scrollbar-track: #f1f1f1; @@ -281,6 +283,8 @@ html.dark-theme, --uplt-color-accent-grad-end: rgba(26, 168, 154, 0.04); --uplt-color-accent-shadow-strong: rgba(26, 168, 154, 0.26); --uplt-color-accent-shadow-soft: rgba(26, 168, 154, 0.14); + --uplt-color-plot-panel-bg: #1b2024; + --uplt-color-plot-panel-border: #313940; --sy-c-link: #58d5c9; --sy-c-link-hover: #84e8df; --uplt-color-panel-bg: #202020; @@ -782,12 +786,18 @@ body.wy-body-for-nav } .yue div.nbinput.container > div.input_area, -.yue div.nboutput.container > div.output_area, .yue .highlight, .yue .highlight pre { background-color: var(--code-block-background) !important; } +.yue div.nboutput.container > div.output_area { + background-color: var(--uplt-color-plot-panel-bg) !important; + border: 1px solid var(--uplt-color-plot-panel-border); + border-radius: 0.45rem; + padding: 0.4rem; +} + /* Shibuya right TOC: collapse sub-H1 headings under each H1 section */ .sy-rside .localtoc { margin-left: 0.55rem; diff --git a/docs/conf.py b/docs/conf.py index 5fcc100af..52f8f46fc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -133,6 +133,20 @@ def __getattr__(self, name): from sphinx_gallery.sorting import ExplicitOrder, FileNameSortKey +def _set_plot_transparency_defaults(): + """ + Use transparent defaults so rendered docs figures adapt to light/dark themes. + """ + try: + import matplotlib as mpl + except Exception: + return + mpl.rcParams["figure.facecolor"] = "none" + mpl.rcParams["axes.facecolor"] = "none" + mpl.rcParams["savefig.facecolor"] = "none" + mpl.rcParams["savefig.edgecolor"] = "none" + + def _reset_ultraplot(gallery_conf, fname): """ Reset UltraPlot rc state between gallery examples. @@ -146,6 +160,10 @@ def _reset_ultraplot(gallery_conf, fname): _logger.setLevel(logging.ERROR) _logger.propagate = False uplt.rc.reset() + _set_plot_transparency_defaults() + + +_set_plot_transparency_defaults() # -- Project information ------------------------------------------------------- @@ -395,6 +413,16 @@ def _reset_ultraplot(gallery_conf, fname): # Add jupytext support to nbsphinx nbsphinx_custom_formats = {".py": ["jupytext.reads", {"fmt": "py:percent"}]} +# Keep notebook output backgrounds theme-adaptive. +nbsphinx_execute_arguments = [ + "--InlineBackend.rc={" + "'figure.facecolor': 'none', " + "'axes.facecolor': 'none', " + "'savefig.facecolor': 'none', " + "'savefig.edgecolor': 'none'" + "}", +] + # Control notebook execution from env for predictable local/CI builds. # Use values: auto, always, never. nbsphinx_execute = os.environ.get("UPLT_DOCS_EXECUTE", "auto").strip().lower() From bb2fb66b391d285a0fcea0e4447c6f86e90a10a4 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 14 Feb 2026 21:55:27 +1000 Subject: [PATCH 09/10] Docs: full-width notebook outputs and add missing font_table anchor --- docs/_static/custom.css | 17 +++++++++++++++++ docs/configuration.rst | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index f1fcaf962..6028770ae 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -791,11 +791,28 @@ body.wy-body-for-nav background-color: var(--code-block-background) !important; } +.yue div.nboutput.container { + display: block !important; +} + +.yue div.nboutput.container > div.prompt { + display: none !important; +} + .yue div.nboutput.container > div.output_area { background-color: var(--uplt-color-plot-panel-bg) !important; border: 1px solid var(--uplt-color-plot-panel-border); border-radius: 0.45rem; padding: 0.4rem; + width: 100%; + max-width: 100%; + margin: 0.25rem 0 !important; + overflow: visible; +} + +.yue div.nboutput.container > div.output_area > * { + margin-left: auto; + margin-right: auto; } /* Shibuya right TOC: collapse sub-H1 headings under each H1 section */ diff --git a/docs/configuration.rst b/docs/configuration.rst index af520ba95..4137f357e 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -150,6 +150,28 @@ Here's a broad overview of the "meta-settings": * Setting :rcraw:`title.border` or :rcraw:`abc.border` to ``True`` automatically sets :rcraw:`title.bbox` or :rcraw:`abc.bbox` to ``False``, and vice versa. +.. _font_table: + +Relative font size table +------------------------ + +When a setting accepts a *relative font size* string, these values are available. +The ``'med'``, ``'med-small'``, and ``'med-large'`` aliases are added by UltraPlot. + +========================== ===== +Size Scale +========================== ===== +``'xx-small'`` 0.579 +``'x-small'`` 0.694 +``'small'``, ``'smaller'`` 0.833 +``'med-small'`` 0.9 +``'med'``, ``'medium'`` 1.0 +``'med-large'`` 1.1 +``'large'``, ``'larger'`` 1.2 +``'x-large'`` 1.440 +``'xx-large'`` 1.728 +========================== ===== + .. _ug_rctable: Table of settings From c1abed61eaa343f0a75cff318aeb367558ed3807 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sat, 14 Feb 2026 22:02:14 +1000 Subject: [PATCH 10/10] Docs: style header brand text with multi-shade green gradient --- docs/_static/custom.css | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 6028770ae..d30a8f0fd 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -88,6 +88,22 @@ letter-spacing: 0.065em; text-transform: uppercase; line-height: 1.25; + color: #138a73; + background-image: linear-gradient( + 90deg, + #0f6d5f 0%, + #11806b 12%, + #139378 24%, + #15a685 36%, + #17b793 48%, + #19a988 60%, + #1a9c7d 72%, + #1c8f73 84%, + #1e8268 100% + ); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } @media (min-width: 768px) { @@ -238,6 +254,24 @@ html.dark-theme .sy-head .sy-head-links a, opacity: 0.96; } +html.dark .sy-head .sy-head-brand strong, +html.dark-theme .sy-head .sy-head-brand strong, +[data-color-mode="dark"] .sy-head .sy-head-brand strong { + color: #6ee0c8; + background-image: linear-gradient( + 90deg, + #47cdb2 0%, + #53d6bc 12%, + #5fdec6 24%, + #6be6d0 36%, + #77edd9 48%, + #6be6d0 60%, + #5fdec6 72%, + #53d6bc 84%, + #47cdb2 100% + ); +} + html.dark .sy-head .sy-head-links a:hover, html.dark-theme .sy-head .sy-head-links a:hover, [data-color-mode="dark"] .sy-head .sy-head-links a:hover {