diff --git a/CHANGES.rst b/CHANGES.rst index 6408e046..0385a01a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 ----------------- diff --git a/package.json b/package.json index ba112001..a39b7c0b 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/crate/theme/rtd/crate/static/css/crateio-rtd.css b/src/crate/theme/rtd/crate/static/css/crateio-rtd.css index 7ea7e656..f7b5006a 100644 --- a/src/crate/theme/rtd/crate/static/css/crateio-rtd.css +++ b/src/crate/theme/rtd/crate/static/css/crateio-rtd.css @@ -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) { @@ -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, diff --git a/src/crate/theme/rtd/crate/static/css/furo-collapsible-toc.scss b/src/crate/theme/rtd/crate/static/css/furo-collapsible-toc.scss index 468190a6..0e075031 100644 --- a/src/crate/theme/rtd/crate/static/css/furo-collapsible-toc.scss +++ b/src/crate/theme/rtd/crate/static/css/furo-collapsible-toc.scss @@ -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 { diff --git a/src/crate/theme/rtd/crate/static/js/custom.js b/src/crate/theme/rtd/crate/static/js/custom.js index 1458daf1..ac2e3448 100644 --- a/src/crate/theme/rtd/crate/static/js/custom.js +++ b/src/crate/theme/rtd/crate/static/js/custom.js @@ -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); @@ -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. @@ -151,7 +150,6 @@ document.addEventListener('DOMContentLoaded', () => { checkbox.checked = true; } - saveNavState(); }); }); @@ -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'); diff --git a/src/crate/theme/rtd/crate/static/js/swup.js b/src/crate/theme/rtd/crate/static/js/swup.js new file mode 100644 index 00000000..853b62df --- /dev/null +++ b/src/crate/theme/rtd/crate/static/js/swup.js @@ -0,0 +1,366 @@ +/** + * Swup Integration Module + * + * Implements client-side page transitions using Swup to avoid full page + * reloads within a documentation section. Only the main content area + * (main.sb-main) is replaced; the sidebar lives outside it and is + * preserved naturally, keeping the user's scroll position intact. + * + * Cross-section navigation falls back to a full page reload; + * sidebar scroll is bridged across that reload via sessionStorage + * (see custom.js). + * + * See: + * - Swup: https://swup.js.org/ + * - Morph Plugin: https://swup.js.org/plugins/morph-plugin + */ + +import Swup from 'swup'; +import SwupMorphPlugin from 'swup-morph-plugin'; + +const SIDEBAR_SELECTOR = '.sidebar-tree'; +const MAIN_CONTAINER_SELECTOR = 'main.sb-main'; +const SWUP_LINK_SELECTOR = `${MAIN_CONTAINER_SELECTOR} a[href], ${SIDEBAR_SELECTOR} a.reference.internal[href], ${SIDEBAR_SELECTOR} a.current-active[href]`; +const DOWNLOADABLE_FILE_EXTENSIONS = /\.(pdf|zip|exe|dmg|tar|gz)$/i; + +// Monotonic token used to ignore stale async sidebar-sync completions when +// multiple Swup navigations happen in quick succession. +let sidebarSyncRunId = 0; + +/** Reinitialize interactive elements after Swup replaces page content. */ +function reinitializeAfterSwap({ pageData = null, allowFetchFallback = true } = {}) { + // Intentional fire-and-forget: sidebar sync should not block page rendering. + void syncSidebarCurrentStateFromFetchedPage(pageData, allowFetchFallback); + + // Trigger any Sphinx-specific initializations + if (window.Sphinx && typeof window.initSearch === 'function') { + try { + window.initSearch(); + } catch { + // Search initialization might not be available + } + } +} + +/** Extract the sidebar-tree element from a Swup page payload. */ +function getFetchedSidebarFromPageData(pageData) { + if (!pageData) { + return null; + } + + if (pageData.document) { + return pageData.document.querySelector(SIDEBAR_SELECTOR); + } + + if (typeof pageData.html === 'string') { + const parsed = new DOMParser().parseFromString(pageData.html, 'text/html'); + return parsed.querySelector(SIDEBAR_SELECTOR); + } + + return null; +} + +/** Check if an href is empty, a fragment, mailto, or javascript URI. */ +function isNonNavigableHref(href) { + return !href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('javascript:'); +} + +/** Resolve an href to a canonical pathname for comparison (strips .html, trailing slash). */ +function normalizeHrefPath(href, baseUrl) { + if (isNonNavigableHref(href)) { + return null; + } + try { + const url = new URL(href, baseUrl); + return url.pathname + .replace(/\/index\.html$/, '/') + .replace(/\.html$/, '') + .replace(/\/$/, ''); + } catch { + return null; + } +} + +/** + * Resolve an href to a local absolute path, or return the href as-is + * for non-navigable/external URLs. + */ +function resolveLocalHref(href, baseUrl) { + if (isNonNavigableHref(href)) { + return href; + } + try { + const url = new URL(href, baseUrl); + if (url.origin !== window.location.origin) { + return href; + } + return `${url.pathname}${url.search}${url.hash}`; + } catch { + return href; + } +} + +/** Convert all navigable sidebar links to absolute local paths. */ +function absolutizeSidebarLinks(baseUrl = window.location.href) { + const sidebar = document.querySelector(SIDEBAR_SELECTOR); + if (!sidebar) { + return; + } + + sidebar.querySelectorAll('a[href]').forEach((anchor) => { + const href = anchor.getAttribute('href') || ''; + if (!isNonNavigableHref(href)) { + anchor.setAttribute('href', resolveLocalHref(href, baseUrl)); + } + }); +} + +/** + * Sync current-state classes from fetched sidebar to live sidebar by index. + * Both sidebars must have the same number of anchors. + */ +function syncClassesByIndex(liveSidebar, liveAnchors, fetchedAnchors) { + const baseUrl = window.location.href; + + liveAnchors.forEach((liveAnchor, index) => { + const fetchedAnchor = fetchedAnchors[index]; + + // Sync hrefs from fetched sidebar to correct relative paths. + // Skip non-navigable values (e.g. '#') that Sphinx renders for the + // current page's own toctree entry: syncing '#' would overwrite the + // live absolute path and cause the next click on that link to fall + // through to a full-page reload via ignoreVisit. + const fetchedHref = fetchedAnchor.getAttribute('href') || ''; + if (!isNonNavigableHref(fetchedHref)) { + liveAnchor.setAttribute('href', resolveLocalHref(fetchedHref, baseUrl)); + } + + liveAnchor.classList.toggle('current', fetchedAnchor.classList.contains('current')); + + const liveItem = liveAnchor.closest('li'); + const fetchedItem = fetchedAnchor.closest('li'); + if (liveItem && fetchedItem) { + liveItem.classList.toggle('current', fetchedItem.classList.contains('current')); + liveItem.classList.toggle('current-page', fetchedItem.classList.contains('current-page')); + } + }); + + // Ensure only one active leaf link is highlighted. + liveSidebar.querySelectorAll('a.current-active').forEach((el) => { + el.classList.remove('current-active'); + }); + const currentPageAnchors = liveSidebar.querySelectorAll('li.current-page > a[href]'); + if (currentPageAnchors.length > 0) { + currentPageAnchors[currentPageAnchors.length - 1].classList.add('current-active'); + } +} + +/** + * Mark an anchor and its ancestor
  • elements with current-state classes. + */ +function markAnchorAsCurrent(anchor, liveSidebar) { + anchor.classList.add('current-active'); + + let cursor = anchor.closest('li'); + let depth = 0; + while (cursor && liveSidebar.contains(cursor) && depth < 20) { + cursor.classList.add('current'); + if (depth === 0) { + cursor.classList.add('current-page'); + } + cursor = cursor.parentElement?.closest('li'); + depth += 1; + } +} + +/** + * Find the live sidebar anchor that corresponds to the fetched sidebar's + * current page. Matches by normalized path first, falls back to text content. + */ +function findCurrentAnchorByFallback(liveSidebar, fetchedSidebar) { + // Identify the deepest current-page anchor in the fetched sidebar. + const fetchedCurrentAnchors = fetchedSidebar.querySelectorAll('li.current-page > a[href]'); + if (fetchedCurrentAnchors.length === 0) { + return null; + } + const fetchedCurrentAnchor = fetchedCurrentAnchors[fetchedCurrentAnchors.length - 1]; + + const baseUrl = window.location.href; + const fetchedHref = fetchedCurrentAnchor.getAttribute('href') || ''; + const fetchedPath = normalizeHrefPath(fetchedHref, baseUrl); + const fetchedText = (fetchedCurrentAnchor.textContent || '').trim(); + + // Scan live sidebar: prefer path match, accept text match as fallback. + let textMatch = null; + for (const anchor of liveSidebar.querySelectorAll('a[href]')) { + const anchorHref = anchor.getAttribute('href') || ''; + const anchorPath = normalizeHrefPath(anchorHref, baseUrl); + + if (fetchedHref !== '#' && anchorHref !== '#' && fetchedPath && anchorPath && fetchedPath === anchorPath) { + return anchor; + } + + if (!textMatch && fetchedText && (anchor.textContent || '').trim() === fetchedText) { + textMatch = anchor; + } + } + + return textMatch; +} + +/** + * Update the live sidebar's current-page highlighting to match the newly + * navigated page. Compares the live sidebar against the fetched page's + * sidebar: uses index-based sync when structures match (fast path), or + * falls back to path/text matching when they differ. + */ +async function syncSidebarCurrentStateFromFetchedPage(pageData = null, allowFetchFallback = true) { + const runId = ++sidebarSyncRunId; + + // Locate the live sidebar in the current document. + const liveSidebar = document.querySelector(SIDEBAR_SELECTOR); + if (!liveSidebar) { + return; + } + + try { + // Prefer Swup's already-fetched page payload to avoid extra requests. + let fetchedSidebar = getFetchedSidebarFromPageData(pageData); + + // Fallback only when page payload is unavailable. + if (!fetchedSidebar && allowFetchFallback) { + const response = await fetch(window.location.href, { + headers: { 'X-Requested-With': 'swup-sidebar-sync' }, + }); + if (!response.ok) { + return; + } + + const html = await response.text(); + const parsed = new DOMParser().parseFromString(html, 'text/html'); + fetchedSidebar = parsed.querySelector(SIDEBAR_SELECTOR); + } + + if (!fetchedSidebar) { + return; + } + + if (runId !== sidebarSyncRunId) { + return; + } + + // Reset current-state classes before applying new ones. + liveSidebar.querySelectorAll('.current, .current-active, .current-page').forEach((el) => { + el.classList.remove('current', 'current-active', 'current-page'); + }); + + const liveAnchors = Array.from(liveSidebar.querySelectorAll('a[href]')); + const fetchedAnchors = Array.from(fetchedSidebar.querySelectorAll('a[href]')); + + // Fast path: synchronize item-by-item when sidebar structures align. + if (liveAnchors.length === fetchedAnchors.length && liveAnchors.length > 0) { + syncClassesByIndex(liveSidebar, liveAnchors, fetchedAnchors); + absolutizeSidebarLinks(window.location.href); + return; + } + + // Fallback: when sidebar structures differ (e.g. cross-project navigation), + // find the current page's anchor in the live sidebar by matching against + // the fetched sidebar's active link. + const liveCurrentAnchor = findCurrentAnchorByFallback(liveSidebar, fetchedSidebar); + if (!liveCurrentAnchor) { + return; + } + + markAnchorAsCurrent(liveCurrentAnchor, liveSidebar); + absolutizeSidebarLinks(window.location.href); + } catch (e) { + console.warn('Swup: Could not sync sidebar current state', e); + } +} + +/** Initialize Swup for client-side navigation within the current docs root. */ +export function initSwup() { + if (!('fetch' in window)) { + console.warn('Swup: Browser does not support fetch API. Falling back to standard page reloads.'); + return null; + } + + if (!document.querySelector(MAIN_CONTAINER_SELECTOR)) { + console.warn('Swup: Could not find main content container (main.sb-main). Skipping initialization.'); + return null; + } + + try { + const rootPath = window.location.pathname.replace(/[^/]*$/, ''); + + const swup = new Swup({ + // Container to swap on page navigation + containers: [MAIN_CONTAINER_SELECTOR], + + // Only intercept links we know belong to this documentation page shell + linkSelector: SWUP_LINK_SELECTOR, + + // Ignore links outside current docs root or unsupported targets + ignoreVisit: (href, { el } = {}) => { + if (!el || !(el instanceof HTMLAnchorElement)) { + return true; + } + + const relAttr = el.getAttribute('rel') || ''; + if (el.target === '_blank' || relAttr.includes('external')) { + return true; + } + + if (el.hasAttribute('download')) { + return true; + } + + const rawHref = el.getAttribute('href') || ''; + if (isNonNavigableHref(rawHref)) { + return true; + } + if (DOWNLOADABLE_FILE_EXTENSIONS.test(rawHref)) { + return true; + } + + let targetUrl; + try { + targetUrl = new URL(href, window.location.origin); + } catch { + return true; + } + + if (targetUrl.origin !== window.location.origin) { + return true; + } + + return !targetUrl.pathname.startsWith(rootPath); + }, + + // Morph main content in-place rather than replacing it wholesale, + // which avoids layout thrash and preserves focus/scroll in the content area. + plugins: [new SwupMorphPlugin()], + + // Disable animation detection warnings when no transition-* classes exist + animationSelector: false, + }); + + absolutizeSidebarLinks(window.location.href); + reinitializeAfterSwap({ allowFetchFallback: false }); + + // Handle post-navigation initialization. + // Note: the sidebar (.sidebar-sticky) lives outside main.sb-main so Swup never + // touches it; its scrollTop is preserved naturally across same-section navigations. + // Cross-section navigations trigger a full page reload; sessionStorage is used + // to restore scroll across that reload (see custom.js). + swup.hooks.on('content:replace', (_visit, { page } = {}) => { + reinitializeAfterSwap({ pageData: page }); + }); + + return swup; + } catch (error) { + console.error('Swup: Initialization failed', error); + return null; + } +} diff --git a/yarn.lock b/yarn.lock index 5396af05..337da678 100644 --- a/yarn.lock +++ b/yarn.lock @@ -171,6 +171,13 @@ "@parcel/watcher-win32-ia32" "2.4.1" "@parcel/watcher-win32-x64" "2.4.1" +"@swup/plugin@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@swup/plugin/-/plugin-4.0.0.tgz#5400eef1a5b90d463f591bde9dd40bf59fea5081" + integrity sha512-3Kq31BJxnzoPg643YxGoWQggoU6VPKZpdE5CqqmP7wwkpCYTzkRmrfcQ29mGhsSS7xfS7D33iZoBiwY+wPoo2A== + dependencies: + swup "^4.0.0" + "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -577,6 +584,13 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +delegate-it@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/delegate-it/-/delegate-it-6.3.0.tgz#43409d44280e5ae1621a84239176f62a59be9f12" + integrity sha512-WAa6cA61M5mfDR31PBgMNQQ3LY1q++TxnZzcm7E9XV8ODBPxDutxH0toTR/BXqIkLaVuU7ntFe1uOqDllhA22A== + dependencies: + typed-query-selector "^2.11.2" + detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -1010,6 +1024,11 @@ modernizr@^3.7.1: requirejs "^2.3.6" yargs "^15.3.1" +morphdom@^2.7.0: + version "2.7.8" + resolved "https://registry.yarnpkg.com/morphdom/-/morphdom-2.7.8.tgz#9a81aae144136702d58cfae8226470bfd64fe74c" + integrity sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg== + nanoid@^3.3.11: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" @@ -1035,6 +1054,11 @@ normalize.css@^8.0.1: resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== +opencollective-postinstall@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" + integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -1086,6 +1110,11 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-to-regexp@^6.2.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -1365,6 +1394,23 @@ supports-color@^8.0.0: dependencies: has-flag "^4.0.0" +swup-morph-plugin@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/swup-morph-plugin/-/swup-morph-plugin-1.3.0.tgz#3995a4c71c4b1d7dc50bcc84076468499d512d00" + integrity sha512-vTqWYA5ZFkWMo54K8jlol5OCvboqRsELLfM1PUkS2IiL+1dDDChzMHa4ZBI5+yfl7bZUCWgd8EmuhMd/i/o+Qg== + dependencies: + "@swup/plugin" "^4.0.0" + morphdom "^2.7.0" + +swup@^4.0.0, swup@^4.8.2: + version "4.8.2" + resolved "https://registry.yarnpkg.com/swup/-/swup-4.8.2.tgz#51e12dad05eb7ea8f0ffb3d60ebff9f545643d48" + integrity sha512-Art2vB4idZ7EFZQhhA47ZifkmZMPgcAwE6z28BhorbTYCO8jcovcc5MasX49GGdXYJWO43DTut7iZb5yrQdEfA== + dependencies: + delegate-it "^6.0.0" + opencollective-postinstall "^2.0.2" + path-to-regexp "^6.2.1" + tapable@^2.2.0, tapable@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" @@ -1398,6 +1444,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +typed-query-selector@^2.11.2: + version "2.12.0" + resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" + integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"