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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ CHANGES
Unreleased
----------
- Navigation: Added references to CrateDB MCP and CrateDB Toolkit
- Silky smooth navigation in TOC - gone are the flickering.
- Above enables animated expand/collapse icons.

2026/02/12 0.49.1
-----------------
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@
"loader-utils": "^1.4.2",
"markdown-it": "^13.0.1",
"minimist": "^1.2.7"
},
"dependencies": {
"swup": "^4.8.2",
"swup-morph-plugin": "^1.3.0"
}
}
7 changes: 3 additions & 4 deletions src/crate/theme/rtd/crate/static/css/crateio-rtd.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ div.hero.danger {
color: var(--color-sidebar-left-link);
padding: 4px 0px;
display: block;
/* Fix: reserve bold-width space so font-weight changes don't shift layout */
min-height: calc(4px * 2 + 14px * 1.2);
}

@media all and (max-width: 540px) {
Expand All @@ -136,10 +138,7 @@ div.hero.danger {
font-weight: 600;
}

.bs-docs-sidenav .toctree ul > li.current > a {
margin-bottom: 3px !important;
display: block;
}
/* margin-bottom removed: it caused extra space under the selected TOC entry. */

@media all and (min-width: 768px) {
.bs-docs-sidenav ol,
Expand Down
10 changes: 10 additions & 0 deletions src/crate/theme/rtd/crate/static/css/furo-collapsible-toc.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ ul.toctree.nav p.caption {
display: inline-block;
}

/*
* Animate the expand/collapse icon rotation when a section is toggled.
* Gated on .toc-animations-ready so the transition does not fire during the
* initial page-load state restoration (where JS sets checkbox.checked
* synchronously before the first paint).
*/
.toc-animations-ready .toctree-checkbox ~ label .icon svg {
transition: transform 200ms ease;
}

.bs-docs-sidenav.sidebar-tree {
/* Match label height to our link height */
li.has-children > label {
Expand Down
186 changes: 101 additions & 85 deletions src/crate/theme/rtd/crate/static/js/custom.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,75 @@
/**
* This JS file is for additional new JS built over the existing theme
* ...so as to avoid breaking anything unexpectedly
* Custom theme JavaScript.
*
* Handles:
* - Sidebar TOC scroll preservation across Swup navigations and full-page reloads
* - Dark/light theme switching (persisted in localStorage)
* - TOC expand/collapse behaviour
* - Swup initialization
*/

var Cookies = require('js-cookie');
import { initSwup } from './swup';

const SIDEBAR_SCROLL_KEY = 'crate-sidebar-scroll';

/**
* Save sidebar scroll position to sessionStorage before page unload.
* This enables scroll preservation across full-page reloads (cross-section navigation).
*/
function saveSidebarScrollBeforeUnload() {
const sidebarSticky = document.querySelector('.sidebar-sticky');
if (sidebarSticky && sidebarSticky.scrollTop > 0) {
try {
sessionStorage.setItem(SIDEBAR_SCROLL_KEY, String(sidebarSticky.scrollTop));
} catch {
// sessionStorage might not be available
}
}
}

/**
* Restore sidebar scroll position from sessionStorage after page load.
* Called after DOMContentLoaded to ensure the sidebar is rendered.
*/
function restoreSidebarScrollFromSessionStorage() {
try {
const savedScroll = sessionStorage.getItem(SIDEBAR_SCROLL_KEY);
if (!savedScroll) {
return;
}
const scrollValue = parseInt(savedScroll, 10);
if (isNaN(scrollValue) || scrollValue <= 0) {
return;
}

// Clear once used (one-shot restore)
sessionStorage.removeItem(SIDEBAR_SCROLL_KEY);

const sidebarSticky = document.querySelector('.sidebar-sticky');
if (sidebarSticky) {
sidebarSticky.scrollTop = scrollValue;
}
} catch {
// sessionStorage might not be available
}
}

document.addEventListener('DOMContentLoaded', () => {
// Restore sidebar scroll before any other initialization
restoreSidebarScrollFromSessionStorage();

// Save sidebar scroll position before navigating away (handles cross-section full reloads)
window.addEventListener('beforeunload', saveSidebarScrollBeforeUnload);

// Initialize Swup for client-side navigation
// Progressive enhancement: if initialization throws, keep standard
// full-page navigation and continue running other page scripts.
try {
window.swup = initSwup();
} catch (error) {
console.warn('Swup initialization failed; falling back to full-page navigation:', error);
}

// Function to set the theme
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
Expand Down Expand Up @@ -37,95 +101,30 @@ document.addEventListener('DOMContentLoaded', () => {
// Initialize theme on page load
initTheme();

//
// Preserve navigation state (left navbar) across page loads.
//
function saveNavState() {
const checkboxes = document.querySelectorAll('.toctree-checkbox');
const states = {};
checkboxes.forEach((checkbox) => {
if (checkbox.id) {
states[checkbox.id] = checkbox.checked;
}
});
try {
localStorage.setItem('navState', JSON.stringify(states));
} catch (e) {
// Could be QuotaExceededError or other storage error
console.warn('Could not save navigation state to localStorage:', e);
}
}

function restoreNavState() {
const savedStates = localStorage.getItem('navState');
if (savedStates) {
let states;
try {
states = JSON.parse(savedStates);
} catch (e) {
// If parsing fails, clear the corrupted data and do not restore state
localStorage.removeItem('navState');
return;
}
Object.keys(states).forEach((id) => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.checked = states[id];
}
});
}
}

// Restore state on page load
restoreNavState();

let stateChanged = false;

// Ensure the path to the current page is always expanded in the TOC.
// When opening a page directly via URL (not by navigating within the
// site), restoreNavState() may set checkboxes to a previously saved
// state that doesn't include the current page's ancestors as expanded.
// Furo marks these ancestors with the "current" class, so we force
// their checkboxes open.
// This matters both for direct URL loads and non-swup fallbacks.

document.querySelectorAll('.sidebar-tree li.current > .toctree-checkbox').forEach((checkbox) => {
if (!checkbox.checked) {
checkbox.checked = true;
stateChanged = true;
}
checkbox.checked = true;
});

// Auto-expand sections marked with data-auto-expand="true"
// Used for Database Drivers when viewing a driver project.
// Only auto-expand if user hasn't explicitly set a preference for this checkbox.
const savedStates = localStorage.getItem('navState');
let userPreferences = {};
if (savedStates) {
try {
userPreferences = JSON.parse(savedStates);
} catch (e) {
// Ignore parse errors, treat as no preferences
}
}

document.querySelectorAll('[data-auto-expand="true"]').forEach((li) => {
const checkbox = li.querySelector('.toctree-checkbox');
if (checkbox && checkbox.id) {
// Only auto-expand if user has no saved preference for this checkbox
if (!(checkbox.id in userPreferences)) {
checkbox.checked = true;
stateChanged = true;
}
if (checkbox) {
checkbox.checked = true;
}
});

// Save state if initialization expanded any sections
if (stateChanged) {
saveNavState();
}

// Save state when checkboxes change
document.querySelectorAll('.toctree-checkbox').forEach((checkbox) => {
checkbox.addEventListener('change', saveNavState);
// Enable icon rotation animations after initial state is restored.
// Using double-rAF to ensure the browser has painted the initial state
// before we add the transition, avoiding an animated jump on page load.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.querySelector('.sidebar-tree')?.classList.add('toc-animations-ready');
});
});

// Make clicking the link text expand the section and collapse siblings.
Expand All @@ -151,7 +150,6 @@ document.addEventListener('DOMContentLoaded', () => {
checkbox.checked = true;
}

saveNavState();
});
});

Expand All @@ -175,10 +173,28 @@ document.addEventListener('DOMContentLoaded', () => {
});
});

// Mark Overview as current on root page. The overview content lives in
// index.md but Sphinx only marks toctree entries as "current" when the
// pagename matches. Since the Overview href is rewritten to "#" (self)
// on the root page, we detect that and add the current class.
// Handle clicks on the "current" page link (marked with .current-active class).
// Uses event delegation so it keeps working after Swup navigations update
// the sidebar's current-active class to a different element.
// - href="#": prevent browser's default fragment scroll; re-navigate via Swup instead
// - Any other href: let Swup (or the browser) handle it normally
document.addEventListener('click', (e) => {
const a = e.target.closest('a.current-active');
if (!a) {
return;
}
if (a.getAttribute('href') === '#') {
e.preventDefault();
if (window.swup) {
window.swup.navigate(window.location.pathname);
}
}
}, false);

// Mark current-state classes for Overview-like entries in sidebar tree
// The overview content lives in index.md but Sphinx only marks toctree entries
// as "current" when the pagename matches. Since the Overview href is rewritten
// to "#" (self) on the root page, we detect that and add the current class.
document.querySelectorAll('.sidebar-tree .toctree-l1 > a[href="#"]').forEach((a) => {
a.classList.add('current');
a.closest('li').classList.add('current');
Expand Down
Loading