From fe1c5a2a11b42daa31a3d61983507e166ba48954 Mon Sep 17 00:00:00 2001 From: Giuseppina Crimi Date: Wed, 5 Aug 2026 13:28:40 +0200 Subject: [PATCH 1/2] fix: align widened content with header grid, sync ToC tooltip state - .md-nav__source: fixed dark badge colors instead of Material's broken indigo fallback (background-color var was never set for this theme) - .md-content divider and .md-grid width cap: scoped to >=76.25em, they were leaking unwanted margin/border onto mobile - .md-content__inner: drops the 90ch reading cap once a sidebar is collapsed (UCD-180 widen feature), instead of staying pinned to it - .md-main__inner: use padding instead of a one-sided margin override so it still centers via margin:auto like the header, keeping both edges aligned when a sidebar collapses - toc-collapse-toggle.js: sync the tooltip text with the button's actual state, it stayed on "ausblenden" even after the ToC was hidden --- .../assets/javascripts/toc-collapse-toggle.js | 26 +++++---- .../stylesheets/mkdocs-doubleslash-theme.css | 57 +++++++++++++------ 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js b/mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js index 7c37092..cdb5077 100644 --- a/mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js +++ b/mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js @@ -7,6 +7,7 @@ var TOOLTIP_DURATION_MS = 3000; var LABEL_COLLAPSE = "Inhaltsverzeichnis ausblenden"; var LABEL_EXPAND = "Inhaltsverzeichnis einblenden"; + var TOOLTIP_PREFIX = "Hier können Sie das "; function isTocCollapsed() { return document.documentElement.hasAttribute("data-ds-toc-collapsed"); @@ -46,15 +47,20 @@ return !secondary.querySelector(".md-nav--secondary .md-nav__item"); } - function updateButtonState(button) { + function updateButtonState(button, tooltip) { var collapsed = isTocCollapsed(); var collapseIcon = button.querySelector(".ds-toc-toggle__icon--collapse"); var expandIcon = button.querySelector(".ds-toc-toggle__icon--expand"); - + var label = collapsed ? LABEL_EXPAND : LABEL_COLLAPSE; + button.setAttribute("aria-pressed", collapsed ? "true" : "false"); - button.setAttribute("aria-label", collapsed ? LABEL_EXPAND : LABEL_COLLAPSE); - button.setAttribute("title", collapsed ? LABEL_EXPAND : LABEL_COLLAPSE); - + button.setAttribute("aria-label", label); + button.setAttribute("title", label); + + if (tooltip) { + tooltip.textContent = TOOLTIP_PREFIX + label; + } + if (collapseIcon) { collapseIcon.hidden = collapsed; } @@ -145,7 +151,7 @@ } if (isTocCollapsed()) { setTocCollapsed(false); - updateButtonState(button); + updateButtonState(button, tooltip); } return; } @@ -200,12 +206,12 @@ } } catch (e) {} - updateButtonState(button); + updateButtonState(button, tooltip); updateButtonVisibility(button, tooltip, tooltipControl); - + button.addEventListener("click", function () { setTocCollapsed(!isTocCollapsed()); - updateButtonState(button); + updateButtonState(button, tooltip); tooltipControl.clearAutoHideTimer(); tooltipControl.hideIfUnpinned(true); document.dispatchEvent(new CustomEvent("ds-toc-collapse-change")); @@ -213,7 +219,7 @@ desktopMedia.addEventListener("change", function () { updateButtonVisibility(button, tooltip, tooltipControl); - updateButtonState(button); + updateButtonState(button, tooltip); }); window.addEventListener("resize", function () { diff --git a/mkdocs_doubleslash_theme/assets/stylesheets/mkdocs-doubleslash-theme.css b/mkdocs_doubleslash_theme/assets/stylesheets/mkdocs-doubleslash-theme.css index cf70df5..75649ad 100644 --- a/mkdocs_doubleslash_theme/assets/stylesheets/mkdocs-doubleslash-theme.css +++ b/mkdocs_doubleslash_theme/assets/stylesheets/mkdocs-doubleslash-theme.css @@ -30,6 +30,7 @@ --ds-spacing-xxl: 24px; --ds-motion-duration: 0.3s; --ds-motion-transition: var(--ds-motion-duration) ease all; + --ds-wide-max-width: min(1800px, 100% - 4rem); /* Typography tokens — source: frontend/DESIGN.md (living-styleguide, ref=main) */ --ds-typography-body-website-font-size: 18px; @@ -157,6 +158,14 @@ border-radius: 0 0 var(--ds-rounded-md) var(--ds-rounded-md); } + /* Material hardcodes this background to --md-primary-fg-color--dark, a + variable this theme never sets, so it fell back to Material's default + indigo. Fixed color in both schemes to match the dark badge look. */ + .md-nav__source { + background-color: var(--ds-color-dark-hover); + color: var(--ds-color-dark-clickable); + } + .md-tabs__item { color: var(--md-typeset-color); opacity: 1; @@ -216,19 +225,25 @@ align-items: stretch; } - .md-content { - border-left: solid; - border-width: 1px; - border-color: var(--ds-color-border); - transition: border-color var(--ds-motion-duration) ease; + /* The sidebar this divider separates from is off-canvas (drawer) below + 76.25em, so the border has nothing to divide until that breakpoint. */ + @media screen and (min-width: 76.25em) { + .md-content { + border-left: solid; + border-width: 1px; + border-color: var(--ds-color-border); + transition: border-color var(--ds-motion-duration) ease; + } } /* Fixed reading measure: as the sidebars collapse/expand, .md-content__inner is a flex item that keeps resizing, so the prose keeps re-wrapping for the whole animation. Capping it stops the reflow and is a readability win on - wide screens either way — .md-grid stays wide for tables/diagrams. */ + wide screens either way — .md-grid stays wide for tables/diagrams. Left + at Material's own small gutter by default, so text still hugs the + sidebar divider instead of floating centered in the available space. */ .md-content__inner { - max-width: 80ch; + max-width: 90ch; } .md-content .md-typeset h1, @@ -397,8 +412,24 @@ width: 1rem; } -.md-grid { - max-width: min(1800px, 100% - 4rem); +/* Only constrain width on wide desktop layouts (>= 76.25em) — unguarded, + the "100% - 4rem" term shrinks these below their natural width on ANY + viewport, adding unwanted side margins on mobile too. Content only picks + up the same width as .md-grid once a sidebar is actually collapsed, so it + stays aligned with the header instead of fighting the 90ch reading cap. */ +@media screen and (min-width: 76.25em) { + .md-grid { + max-width: var(--ds-wide-max-width); + } + + /* Once a sidebar is actually collapsed, drop the 90ch reading cap so text + fills the reclaimed space instead of leaving it padded out on one + side — that's the whole point of the widen toggle. Material's own + small per-sidebar gutter (margin-left/right: 1.2rem) still applies. */ + html[data-ds-content-wide] .md-content__inner, + html[data-ds-toc-collapsed] .md-content__inner { + max-width: none; + } } /* The wrapper (button + tooltip) has no layout below 60em, where the toggle @@ -441,10 +472,6 @@ visibility 0s linear var(--ds-motion-duration); } - html[data-ds-toc-collapsed] .md-main__inner { - margin-right: 80px; - } - .ds-toc-toggle-wrapper { position: fixed; top: 6rem; @@ -594,10 +621,6 @@ visibility 0s linear var(--ds-motion-duration); } - html[data-ds-content-wide] .md-main__inner { - margin-left: 0; - } - html[data-ds-content-wide] .md-content { border-left-color: transparent; } From 10eafed9442d6a04b0c30fc90ade1ef63312323e Mon Sep 17 00:00:00 2001 From: Giuseppina Crimi Date: Wed, 5 Aug 2026 21:08:43 +0200 Subject: [PATCH 2/2] fix: keep sidebar toggles outside the grid, in sync with each other MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Toggle buttons now sit outside the relevant sidebar (or outside .md-main__inner once collapsed) instead of straddling the divider, with a 20px gap from whichever edge is closer — never overlapping the ToC/nav's own links or the "edit this page" icon - .md-content padding-left: 40px removed, it only existed to clear the old straddling button and left content not flush with the header - .md-content__inner: max-width transitions smoothly now (was jumping instantly between 90ch and an unanimatable `none`), and drops the 90ch cap whenever the ToC sidebar is actually visible, not just when a sidebar has been toggled - Toggle border color: fixed an invalid `border` shorthand that silently dropped the "active" border, and two dark-mode selectors that combined data-md-color-scheme with html[data-ds-...] and could never match since Material sets that attribute on , not — both toggles now use the already scheme-aware var(--md-accent-fg-color) and render identically in light/dark, open/collapsed --- .../javascripts/content-width-toggle.js | 26 +++++-- .../assets/javascripts/toc-collapse-toggle.js | 67 +++++++++++++++++-- .../stylesheets/mkdocs-doubleslash-theme.css | 57 +++++++++++----- 3 files changed, 122 insertions(+), 28 deletions(-) diff --git a/mkdocs_doubleslash_theme/assets/javascripts/content-width-toggle.js b/mkdocs_doubleslash_theme/assets/javascripts/content-width-toggle.js index f8e6241..0c126da 100644 --- a/mkdocs_doubleslash_theme/assets/javascripts/content-width-toggle.js +++ b/mkdocs_doubleslash_theme/assets/javascripts/content-width-toggle.js @@ -62,19 +62,31 @@ } function updateButtonPosition(button) { - if (isWideMode() || button.hidden) { + if (button.hidden) { button.style.removeProperty("left"); return; } - - var content = document.querySelector(".md-content"); - if (!content) { + + // Sit just outside the relevant left boundary — the primary sidebar + // while it's still showing (straddling the divider used to overlap + // the nav's own links on short pages), or .md-main__inner (aligned + // with the header grid) once the sidebar is collapsed. Never inside + // either box, so it never covers real content. + var wide = isWideMode(); + var anchor = document.querySelector(wide ? ".md-main__inner" : ".md-sidebar--primary"); + if (!anchor) { button.style.removeProperty("left"); return; } - - // Align with .md-content border-left (the divider belongs to content, not the sidebar). - button.style.left = content.getBoundingClientRect().left + "px"; + + var width = button.getBoundingClientRect().width; + // Extra 20px breathing room from the grid edge, but only when parked + // there in wide mode — flush against the sidebar divider otherwise. + // Never closer than 20px to the real viewport edge either, or the + // button ends up pinned to it on viewports with little/no margin. + var gap = wide ? 20 : 0; + var left = Math.max(20, anchor.getBoundingClientRect().left - width - gap); + button.style.left = left + "px"; } function scheduleButtonPositionUpdate(button) { diff --git a/mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js b/mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js index cdb5077..e76873f 100644 --- a/mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js +++ b/mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js @@ -139,6 +139,40 @@ }; } + function updateWrapperPosition(wrapper) { + if (!wrapper) { + return; + } + + // Sit just outside the relevant right boundary — the secondary + // sidebar while it's still showing (touching the ToC's own right + // edge, not the divider — sitting on the divider used to overlap + // both the "edit this page" icon in content and the ToC's own nav + // links on short pages), or .md-main__inner (aligned with the header + // grid) once it's collapsed. Never inside either box. + var collapsed = isTocCollapsed(); + var anchor = document.querySelector(collapsed ? ".md-main__inner" : ".md-sidebar--secondary"); + if (!anchor) { + wrapper.style.removeProperty("right"); + return; + } + + var width = wrapper.getBoundingClientRect().width; + // Extra 20px breathing room from the grid edge, but only when parked + // there in the collapsed state — flush against the ToC divider + // otherwise. Never closer than 20px to the real viewport edge either, + // or the wrapper ends up pinned to it on viewports with little margin. + var gap = collapsed ? 20 : 0; + var right = Math.max(20, window.innerWidth - anchor.getBoundingClientRect().right - width - gap); + wrapper.style.right = right + "px"; + } + + function scheduleWrapperPositionUpdate(wrapper) { + window.requestAnimationFrame(function () { + updateWrapperPosition(wrapper); + }); + } + function updateButtonVisibility(button, tooltip, tooltipControl) { var wasVisible = !button.hidden; var shouldShow = !tocIsUnavailable(); @@ -193,38 +227,61 @@ function init() { var button = document.querySelector("[data-md-component='toc-collapse-toggle']"); var tooltip = document.querySelector("[data-md-component='toc-collapse-tooltip']"); + var wrapper = document.querySelector(".ds-toc-toggle-wrapper"); if (!button) { return; } - + var tooltipControl = bindTooltipInteractions(button, tooltip); var desktopMedia = window.matchMedia(DESKTOP_QUERY); - + try { if (localStorage.getItem(STORAGE_KEY) === "true") { setTocCollapsed(true); } } catch (e) {} - + updateButtonState(button, tooltip); updateButtonVisibility(button, tooltip, tooltipControl); + updateWrapperPosition(wrapper); button.addEventListener("click", function () { setTocCollapsed(!isTocCollapsed()); updateButtonState(button, tooltip); + updateWrapperPosition(wrapper); tooltipControl.clearAutoHideTimer(); tooltipControl.hideIfUnpinned(true); + // A mouse click also focuses the button, and the focus handler + // below shows the tooltip again with no auto-hide — blur so the + // click's force-hide actually sticks. + button.blur(); document.dispatchEvent(new CustomEvent("ds-toc-collapse-change")); }); - + desktopMedia.addEventListener("change", function () { updateButtonVisibility(button, tooltip, tooltipControl); updateButtonState(button, tooltip); + updateWrapperPosition(wrapper); }); - + window.addEventListener("resize", function () { updateButtonVisibility(button, tooltip, tooltipControl); + updateWrapperPosition(wrapper); + }); + + document.addEventListener("ds-content-wide-change", function () { + scheduleWrapperPositionUpdate(wrapper); }); + + if (typeof ResizeObserver !== "undefined") { + var resizeObserver = new ResizeObserver(function () { + scheduleWrapperPositionUpdate(wrapper); + }); + var content = document.querySelector(".md-content"); + if (content) { + resizeObserver.observe(content); + } + } } if (document.readyState === "loading") { diff --git a/mkdocs_doubleslash_theme/assets/stylesheets/mkdocs-doubleslash-theme.css b/mkdocs_doubleslash_theme/assets/stylesheets/mkdocs-doubleslash-theme.css index 75649ad..0c7e0d9 100644 --- a/mkdocs_doubleslash_theme/assets/stylesheets/mkdocs-doubleslash-theme.css +++ b/mkdocs_doubleslash_theme/assets/stylesheets/mkdocs-doubleslash-theme.css @@ -244,6 +244,7 @@ sidebar divider instead of floating centered in the available space. */ .md-content__inner { max-width: 90ch; + transition: max-width var(--ds-motion-duration) ease; } .md-content .md-typeset h1, @@ -425,10 +426,19 @@ /* Once a sidebar is actually collapsed, drop the 90ch reading cap so text fills the reclaimed space instead of leaving it padded out on one side — that's the whole point of the widen toggle. Material's own - small per-sidebar gutter (margin-left/right: 1.2rem) still applies. */ + small per-sidebar gutter (margin-left/right: 1.2rem) still applies. + Reuses the same cap as .md-grid (a concrete value, not `none`) so the + max-width transition can interpolate smoothly instead of jumping. */ html[data-ds-content-wide] .md-content__inner, html[data-ds-toc-collapsed] .md-content__inner { - max-width: none; + max-width: var(--ds-wide-max-width); + } + + /* The ToC sidebar already bounds the layout on the right, so text can + fill toward it without feeling unbounded — only cap the reading + measure when there's no ToC there to fill toward. */ + [dir="ltr"] .md-sidebar--secondary:not([hidden]) ~ .md-content > .md-content__inner { + max-width: var(--ds-wide-max-width); } } @@ -487,7 +497,7 @@ height: var(--ds-spacing-control-height); padding: 0; margin: 5px 0 0 0; - border: inset 0 0 0 2px var(--ds-color-primary); + border: 2px solid var(--ds-color-border); border-radius: 50px; background-color: var(--md-default-bg-color); color: var(--md-default-fg-color); @@ -495,8 +505,12 @@ transition: var(--ds-motion-transition); } - [data-md-color-scheme="slate"] .ds-toc-toggle { - border-color: var(--ds-color-dark-clickable); + /* var(--md-accent-fg-color): already scheme-aware (aliases to the light + or dark accent per [data-md-color-scheme]), unlike a separate slate + override here — Material puts that attribute on , not , + so a selector combining it with html[data-ds-toc-collapsed] never matches. */ + html[data-ds-toc-collapsed] .ds-toc-toggle { + border: 2px solid var(--md-accent-fg-color); } .ds-toc-toggle:hover { @@ -590,10 +604,6 @@ } @media screen and (min-width: 76.25em) { - .md-content { - padding-left: 40px; - } - .md-sidebar--primary, .md-sidebar--secondary { transition: @@ -603,6 +613,23 @@ visibility 0s linear 0s; } + /* Fixed width so the nav text doesn't reflow as the sidebar's own width + animates to/from 0 — the outer sidebar just clips it via overflow, + instead of the text re-wrapping line by line during the transition. + .md-sidebar__scrollwrap has its own 0.2rem margin on each side, so + matching the sidebar's own 12.1rem here overflowed it horizontally. */ + .md-sidebar__inner { + width: calc(12.1rem - 0.4rem); + } + + /* Material only sets overflow-y on the scrollwrap, which per spec forces + overflow-x to auto too — while the sidebar's width is mid-transition, + the fixed-width inner above briefly overflows it, flashing a + horizontal scrollbar. We never want horizontal scroll here anyway. */ + .md-sidebar__scrollwrap { + overflow-x: hidden; + } + .md-main__inner { transition: margin var(--ds-motion-duration) ease; } @@ -630,7 +657,6 @@ position: fixed; top: 50%; margin-top: calc(var(--ds-toggle-size) / -2); - margin-left: calc(var(--ds-toggle-size) / -2); display: flex; align-items: center; justify-content: center; @@ -685,17 +711,16 @@ display: none; } + /* var(--md-accent-fg-color): already scheme-aware (aliases to the light + or dark accent per [data-md-color-scheme]), unlike a separate slate + override here — Material puts that attribute on , not , + so a selector combining it with html[data-ds-content-wide] never matches. */ html[data-ds-content-wide] .ds-content-width-toggle { - left: var(--ds-spacing-xl); margin-left: 0; - border: inset 0 0 0 2px var(--ds-color-primary); + border: 2px solid var(--md-accent-fg-color); border-radius: 50px; } - [data-md-color-scheme="slate"] html[data-ds-content-wide] .ds-content-width-toggle { - border-color: var(--ds-color-dark-clickable); - } - @media (prefers-reduced-motion: reduce) { .md-sidebar--primary, .md-sidebar--secondary,