Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
93 changes: 78 additions & 15 deletions mkdocs_doubleslash_theme/assets/javascripts/toc-collapse-toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -133,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();
Expand All @@ -145,7 +185,7 @@
}
if (isTocCollapsed()) {
setTocCollapsed(false);
updateButtonState(button);
updateButtonState(button, tooltip);
}
return;
}
Expand Down Expand Up @@ -187,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);

updateButtonState(button, tooltip);
updateButtonVisibility(button, tooltip, tooltipControl);

updateWrapperPosition(wrapper);

button.addEventListener("click", function () {
setTocCollapsed(!isTocCollapsed());
updateButtonState(button);
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);
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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -216,19 +225,26 @@
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;
transition: max-width var(--ds-motion-duration) ease;
}

.md-content .md-typeset h1,
Expand Down Expand Up @@ -397,8 +413,33 @@
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.
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: 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);
}
}

/* The wrapper (button + tooltip) has no layout below 60em, where the toggle
Expand Down Expand Up @@ -441,10 +482,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;
Expand All @@ -460,16 +497,20 @@
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);
cursor: pointer;
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 <body>, not <html>,
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 {
Expand Down Expand Up @@ -563,10 +604,6 @@
}

@media screen and (min-width: 76.25em) {
.md-content {
padding-left: 40px;
}

.md-sidebar--primary,
.md-sidebar--secondary {
transition:
Expand All @@ -576,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;
}
Expand All @@ -594,10 +648,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;
}
Expand All @@ -607,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;
Expand Down Expand Up @@ -662,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 <body>, not <html>,
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,
Expand Down
Loading