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
91 changes: 73 additions & 18 deletions mintlify/sidebar-toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,67 @@
updateRail();
}

// Deliberate, animated toggle — shared by the edge rail and the footer
// button. ls-nav-animating turns the collapse transition on for this toggle
// (it's off by default so navigation never animates) and suppresses the
// hover reveal so the button/edge don't flash from the cursor sitting over
// the rail mid-transition. Add it + force a reflow before applyState so the
// width/opacity change animates from the current value, not snaps.
var animTimer = 0;
function animatedToggle() {
var next = !isCollapsed();
setPref(next ? '1' : '0');
// Mute the rail tooltip: its copy flips with the state (Collapse ⇄
// Expand), so it must not reappear under a cursor that just sat through
// the click with the swapped text. A fresh rail mouseenter unmutes.
document.documentElement.classList.add('ls-nav-tip-muted');
document.documentElement.classList.add('ls-nav-animating');
document.documentElement.getBoundingClientRect(); // force reflow to arm the transition
applyState(next);
clearTimeout(animTimer);
animTimer = setTimeout(function () {
document.documentElement.classList.remove('ls-nav-animating');
}, 320);
}

function removeRail() {
if (rail && rail.parentNode) rail.parentNode.removeChild(rail);
rail = null;
}

// Collapse button in the sidebar footer, next to the theme switcher — the
// discoverable affordance (the edge rail only reveals on hover). This
// element is only the invisible HIT TARGET: the visible circle + glyph are
// pseudo-elements of the server-rendered switcher (see style.css), so the
// button shows from first paint instead of popping in when this script
// finally runs. Lives inside #sidebar-content so the collapsed state hides
// it with the rest of the footer; reopening is the rail's job.
var footerBtn = null;

function removeFooterBtn() {
if (footerBtn && footerBtn.parentNode) footerBtn.parentNode.removeChild(footerBtn);
footerBtn = null;
}

function ensureFooterBtn() {
if (!isDesktop() || !hasVisibleSidebar()) {
removeFooterBtn();
return;
}
if (footerBtn && document.body.contains(footerBtn)) return;
var theme = document.querySelector(
'#sidebar-content button[aria-label="Toggle dark mode"]'
);
if (!theme || !theme.parentElement) return;
footerBtn = document.createElement('button');
footerBtn.type = 'button';
footerBtn.className = 'ls-nav-collapse-btn';
footerBtn.setAttribute('aria-label', 'Hide navigation');
footerBtn.addEventListener('click', animatedToggle);
// Rightmost in the footer row: [spacer][theme switcher][collapse].
theme.parentElement.insertBefore(footerBtn, theme.nextSibling);
}

function ensureRail() {
if (!isDesktop() || !hasVisibleSidebar()) {
removeRail();
Expand All @@ -109,7 +165,6 @@
var moved = false;
var dragging = false;
var dragEndAt = 0;
var animTimer = 0;

function onMove(e) {
if (!moved && Math.abs(e.clientX - startX) > DRAG_THRESHOLD) {
Expand Down Expand Up @@ -145,6 +200,11 @@
}
}

// A genuine (re-)entry re-arms the tooltip muted by animatedToggle.
el.addEventListener('mouseenter', function () {
document.documentElement.classList.remove('ls-nav-tip-muted');
});

// Resize only from the expanded edge; the collapsed rail is click-only.
el.addEventListener('mousedown', function (e) {
if (e.button !== 0 || isCollapsed()) return;
Expand All @@ -158,20 +218,7 @@

el.addEventListener('click', function () {
if (Date.now() - dragEndAt < 300) return; // swallow the click after a drag
var next = !isCollapsed();
setPref(next ? '1' : '0');
// ls-nav-animating turns the collapse transition on for this deliberate
// toggle (it's off by default so navigation never animates) and suppresses
// the hover reveal so the button/edge don't flash from the cursor sitting
// over the rail mid-transition. Add it + force a reflow before applyState
// so the width/opacity change animates from the current value, not snaps.
document.documentElement.classList.add('ls-nav-animating');
document.documentElement.getBoundingClientRect(); // force reflow to arm the transition
applyState(next);
clearTimeout(animTimer);
animTimer = setTimeout(function () {
document.documentElement.classList.remove('ls-nav-animating');
}, 320);
animatedToggle();
});
}

Expand All @@ -185,6 +232,7 @@
root.classList.add('ls-nav-snap');
applyState(shouldCollapse());
ensureRail();
ensureFooterBtn();
requestAnimationFrame(function () {
root.classList.remove('ls-nav-snap');
});
Expand All @@ -203,20 +251,26 @@
// rail never lingers visibly even without per-frame polling here.
var lastPath = location.pathname;
var ensureScheduled = false;
function scheduleEnsureRail() {
function scheduleEnsure() {
if (ensureScheduled) return;
ensureScheduled = true;
requestAnimationFrame(function () {
ensureScheduled = false;
ensureRail();
ensureFooterBtn();
});
}
var observer = new MutationObserver(function () {
if (location.pathname !== lastPath) {
lastPath = location.pathname;
sync();
} else if (!rail || !document.body.contains(rail)) {
scheduleEnsureRail();
} else if (
!rail ||
!document.body.contains(rail) ||
!footerBtn ||
!document.body.contains(footerBtn)
) {
scheduleEnsure();
}
});
observer.observe(document.body, { childList: true, subtree: true });
Expand All @@ -229,6 +283,7 @@
requestAnimationFrame(function () {
rafPending = false;
ensureRail();
ensureFooterBtn();
});
});
})();
221 changes: 220 additions & 1 deletion mintlify/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,12 @@ html.dark {
regular border-radius in unsupported browsers.
=========================================== */

* {
/* `*` matches ELEMENTS only — pseudo-elements need naming explicitly, or
CSS-drawn chrome (tooltips, the footer collapse button's circle, tags)
stays plainly round while everything around it superellipses. */
*,
*::before,
*::after {
corner-shape: squircle;
}

Expand Down Expand Up @@ -4462,6 +4467,220 @@ html.ls-nav-collapsed .ls-nav-rail-btn::before {
mask-image: url("/images/icons/IconSidebarRightArrow.svg");
}

/* FOOTER collapse button, next to the theme switcher — the always-visible,
discoverable affordance (the edge rail only reveals on hover). Chrome
matches the switcher: same hairline border + hover step, same icon color,
28px (h-7) tall; a circle, since it's one button, not a two-state switch.

Split on purpose: the PIXELS are pseudo-elements of the server-rendered
switcher, so the button is visible in the page's first frame — while the
BEHAVIOR is an invisible hit-target button that sidebar-toggle.js mounts
over them a couple hundred ms later (Mintlify inlines custom JS after
paint; nothing of ours can be in the SSR HTML). Drawing on the injected
button instead would pop it in visibly late next to the already-painted
switcher. Hover rides :has(+ .ls-nav-collapse-btn:hover) back onto the
pseudos, so the pixels have exactly one owner. */

/* Reserve the slot: 28px circle + 8px gap. A permanent margin — the footer
never reflows; the pseudos below paint into it from the first frame. */
#sidebar-content > div > button[aria-label="Toggle dark mode"] {
margin-right: 36px;
}
Comment on lines +4486 to +4488

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Pseudo Button Anchors Wrong

The new collapse-button pixels are absolute ::before and ::after children of the theme switcher, but this rule does not make the switcher their containing block. On the first paint, left: calc(100% + 8px) can resolve against the footer row instead of the switcher, placing the visible circle away from the invisible .ls-nav-collapse-btn hit target or clipping it at the sidebar edge.

Suggested change
#sidebar-content > div > button[aria-label="Toggle dark mode"] {
margin-right: 36px;
}
#sidebar-content > div > button[aria-label="Toggle dark mode"] {
position: relative;
margin-right: 36px;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/style.css
Line: 4486-4488

Comment:
**Pseudo Button Anchors Wrong**

The new collapse-button pixels are absolute `::before` and `::after` children of the theme switcher, but this rule does not make the switcher their containing block. On the first paint, `left: calc(100% + 8px)` can resolve against the footer row instead of the switcher, placing the visible circle away from the invisible `.ls-nav-collapse-btn` hit target or clipping it at the sidebar edge.

```suggestion
#sidebar-content > div > button[aria-label="Toggle dark mode"] {
  position: relative;
  margin-right: 36px;
}
```

How can I resolve this? If you propose a fix, please make it concise.


/* The circle, hung off the switcher's right edge (its border box ends where
the reserved margin starts). pointer-events: none so that before the hit
target mounts, a click on the drawn button can't reach the SWITCHER and
flip the theme. */
#sidebar-content > div > button[aria-label="Toggle dark mode"]::after {
content: "";
position: absolute;
left: calc(100% + 8px);
top: calc(50% - 14px);
width: 28px;
height: 28px;
box-sizing: border-box;
border-radius: 9999px;
border: 1px solid rgb(223 223 223 / 0.7); /* border-gray-200/70 */
pointer-events: none;
transition: border-color 150ms ease;
}

/* The glyph, centered in the circle ((28 − 18) / 2 = 5px in). Masked at
18px: the icon is drawn on a 24-grid with a 16-unit body, so this renders
a ~12px glyph at 1.125px effective stroke — the same visual size and
weight as the switcher's 12px sun/moon (16-grid, 1.5 stroke). */
#sidebar-content > div > button[aria-label="Toggle dark mode"]::before {
content: "";
position: absolute;
left: calc(100% + 13px);
top: calc(50% - 9px);
width: 18px;
height: 18px;
background-color: rgb(80 80 80); /* text-gray-600, same in both modes */
-webkit-mask: url("/images/icons/IconSidebarLeftArrow.svg") center / 18px
18px no-repeat;
mask: url("/images/icons/IconSidebarLeftArrow.svg") center / 18px 18px
no-repeat;
pointer-events: none;
transition: background-color 150ms ease;
}

html.dark
#sidebar-content
> div
> button[aria-label="Toggle dark mode"]::after {
border-color: rgb(255 255 255 / 0.07); /* dark:border-white/[0.07] */
}

/* Hovering the hit target: border steps like the switcher's own hover; the
icon goes full-contrast like the edge rail button's. */
#sidebar-content
> div
> button[aria-label="Toggle dark mode"]:has(+ .ls-nav-collapse-btn:hover)::after {
border-color: rgb(223 223 223); /* hover:border-gray-200 */
}

#sidebar-content
> div
> button[aria-label="Toggle dark mode"]:has(+ .ls-nav-collapse-btn:hover)::before {
background-color: var(--ls-gray-950);
}

html.dark
#sidebar-content
> div
> button[aria-label="Toggle dark mode"]:has(+ .ls-nav-collapse-btn:hover)::after {
border-color: rgb(255 255 255 / 0.1); /* dark:hover:border-white/10 */
}

html.dark
#sidebar-content
> div
> button[aria-label="Toggle dark mode"]:has(+ .ls-nav-collapse-btn:hover)::before {
background-color: var(--ls-gray-050);
}

/* The hit target — invisible; the pseudos above are its rendering. Anchored
to the footer's 24px side padding, landing exactly on the drawn circle. */
#sidebar-content > div:has(> button[aria-label="Toggle dark mode"]) {
position: relative;
}

.ls-nav-collapse-btn {
position: absolute;
right: 24px;
top: calc(50% - 14px);
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 9999px; /* round focus ring */
background: transparent;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
}

/* Tooltips — the playground's "coming soon" tag vocabulary (mono micro-type,
hairline edge, soft shadow) but INVERTED like a system tooltip: near-black
bubble with primary-light text in light mode, flipped in dark. One shared
bubble style; placement + copy per trigger below. Free real estate: the
footer hit target renders nothing itself and the rail button's icon is its
::before, so both ::afters are available. */
.ls-nav-collapse-btn::after,
.ls-nav-rail-btn::after {
position: absolute;
padding: 4px 7px;
border: 0.5px solid rgb(255 255 255 / 0.14);
border-radius: 6px;
background: var(--ls-gray-950);
color: var(--ls-gray-050);
font-family: "Suisse Intl Mono", ui-monospace, SFMono-Regular, Menlo,
Consolas, monospace;
font-size: 10px;
line-height: 14px;
letter-spacing: 0.02em;
/* Suisse Mono's bold is too heavy — stroke the glyphs to nudge the weight
up a hair (same trick as the playground's tag). */
-webkit-text-stroke: 0.3px currentColor;
white-space: nowrap;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.06),
0 4px 12px rgba(0, 0, 0, 0.1);
opacity: 0;
pointer-events: none;
/* This is the EXIT transition (the rule that applies once :hover drops).
A plain ease, not the snappy curve — easeOutSnappy front-loads ~90% of
the change into the first few frames, which reads as vanishing
instantly. The entry (snappy + hover-intent delay) lives on the :hover
rules. */
transition: opacity 220ms ease, transform 220ms ease;
}

html.dark .ls-nav-collapse-btn::after,
html.dark .ls-nav-rail-btn::after {
border-color: var(--ls-black-10);
background: var(--ls-gray-050);
color: var(--ls-gray-950);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.3),
0 4px 12px rgba(0, 0, 0, 0.4);
}

/* Copy is one word — the icon already depicts the sidebar, and the footer
bubble must fit centered inside #sidebar-content (overflow-x: hidden clips
anything past the sidebar's right edge; "Collapse sidebar" wouldn't fit). */

/* Footer button: centered ABOVE (beside it, the bubble would sit on top of
the theme switcher), rising up into place — it sits above, so it enters
on the same axis. Hover-intent: in after a beat, soft fade on leave. */
.ls-nav-collapse-btn::after {
content: "Collapse";
left: 50%;
bottom: calc(100% + 8px);
transform: translateX(-50%) translateY(4px);
}

.ls-nav-collapse-btn:hover::after {
opacity: 1;
transform: translateX(-50%) translateY(0);
transition: opacity 180ms var(--ls-sidebar-ease) 350ms,
transform 180ms var(--ls-sidebar-ease) 350ms;
}

/* Rail button: out of the icon to the RIGHT, in both states — the copy
flips with the direction the click would take. */
.ls-nav-rail-btn::after {
content: "Collapse";
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%) translateX(-6px);
}

html.ls-nav-collapsed .ls-nav-rail-btn::after {
content: "Expand";
}

.ls-nav-rail:hover .ls-nav-rail-btn::after {
opacity: 1;
transform: translateY(-50%) translateX(0);
transition: opacity 180ms var(--ls-sidebar-ease) 350ms,
transform 180ms var(--ls-sidebar-ease) 350ms;
}

/* Never mid-drag (the edge is a resize handle first). And on a toggle, hide
INSTANTLY (no fade) — the copy flips with the state, and a fading bubble
would visibly swap Collapse ↔ Expand mid-air. ls-nav-tip-muted then keeps
it hidden until the cursor leaves and genuinely re-enters the rail
(sidebar-toggle.js), so the tooltip never reappears under a cursor that
just sat through the click. */
html.ls-nav-dragging .ls-nav-rail-btn::after,
html.ls-nav-animating .ls-nav-rail-btn::after,
html.ls-nav-tip-muted .ls-nav-rail-btn::after {
opacity: 0 !important;
transition: none !important;
}

/* Hover/drag edge highlight: a 2px line at the divider, drawn as an overlay so
it never shifts layout (the real border stays 0.5px). Expanded only. */
.ls-nav-rail::after {
Expand Down
Loading