diff --git a/docs/_config.yml b/docs/_config.yml index 0a4deb4..99a2d6d 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -68,6 +68,9 @@ callouts: # For copy button on code enable_copy_code_button: true +# Configure heading anchor links +heading_anchors: true + # Waiting until it's needed. # # By default, consuming the theme as a gem leaves mermaid disabled; it is opt-in # mermaid: diff --git a/docs/_includes/head_custom.html b/docs/_includes/head_custom.html index 7ccf75d..c3bcc82 100644 --- a/docs/_includes/head_custom.html +++ b/docs/_includes/head_custom.html @@ -1,3 +1,7 @@ - + + + + diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss index 2b871b5..829d346 100644 --- a/docs/_sass/custom/custom.scss +++ b/docs/_sass/custom/custom.scss @@ -1,3 +1,429 @@ .site-logo { padding-right: 3rem; } + +/* Hide the original search container in main header */ +#main-header .search { + display: none; +} + +/* Show search container in modal */ +.search-modal .search { + display: flex !important; +} + +/* Style the compact search trigger button */ +.search-modal-trigger-wrapper { + display: flex; + align-items: center; +} + +#main-header .search-modal-trigger { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + color: #495057; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s ease; +} + +/* Page TOC (Table of Contents) */ +.page-toc { + position: fixed; + right: 2rem; + top: 6rem; + width: 250px; + max-height: calc(100vh - 8rem); + overflow-y: auto; + background-color: #fff; + border: 1px solid #e8e8e8; + border-radius: 6px; + padding: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + font-size: 0.875rem; + z-index: 10; +} + +.page-toc-heading { + font-weight: 600; + font-size: 0.9rem; + color: #333; + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #e8e8e8; +} + +.page-toc-list { + list-style: none; + padding: 0; + margin: 0; +} + +.page-toc-item { + margin: 0.25rem 0; +} + +.page-toc-link { + display: block; + color: #666; + text-decoration: none; + padding: 0.25rem 0; + transition: color 0.2s ease; +} + +.page-toc-link:hover { + color: #0050d0; +} + +.page-toc-link.active { + color: #0050d0 !important; + font-weight: 500; + border-left: 2px solid #0050d0; + padding-left: 0.5rem; +} + +/* Dark mode support */ +html.dark-mode .page-toc { + background-color: #1a1a1a !important; + border-color: #333 !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important; +} + +html.dark-mode .page-toc-heading { + color: #e0e0e0 !important; + border-color: #333 !important; +} + +html.dark-mode .page-toc-link { + color: #a0a0a0 !important; +} + +html.dark-mode .page-toc-link:hover { + color: #7dd3fc !important; +} + +html.dark-mode .page-toc-link.active { + color: #7dd3fc !important; + border-color: #7dd3fc !important; +} + +/* Responsive - hide TOC on smaller screens */ +@media screen and (max-width: 1280px) { + .page-toc { + display: none; + } +} + +/* Smooth scrolling for anchor links */ +html { + scroll-behavior: smooth; +} + +/* Search Modal Styles */ +.search-modal-trigger-wrapper { + display: flex; + align-items: center; +} + +.search-modal-trigger { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + color: #495057; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.search-modal-trigger:hover { + background: #e9ecef; + border-color: #adb5bd; +} + +.search-modal-trigger svg { + flex-shrink: 0; +} + +.search-modal-shortcut { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.375rem; + background: #fff; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + color: #6c757d; +} + +/* Modal Overlay */ +.search-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 99998; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.search-modal-overlay.active { + opacity: 1; + visibility: visible; +} + +/* Modal Container */ +.search-modal { + position: fixed; + top: 10%; + left: 50%; + transform: translateX(-50%) translateY(-20px); + width: 90%; + max-width: 700px; + max-height: 80vh; + background: #fff; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + z-index: 99999; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.search-modal.active { + opacity: 1; + visibility: visible; + transform: translateX(-50%) translateY(0); +} + +/* Native search component in modal */ +.search-modal .search { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + height: auto !important; + padding: 1rem !important; +} + +/* Show search components when in modal */ +.search-modal .search .search-input-wrap { + display: flex !important; + width: 100%; + flex-shrink: 0; + height: auto !important; + position: relative !important; + max-width: none !important; + overflow: visible !important; + padding: 0 !important; +} + +/* Override search input styles */ +.search-modal .search .search-input { + position: relative !important; + width: 100%; + height: auto !important; + padding: 0.75rem 1rem 0.75rem 2.75rem !important; + font-size: 1rem; + border: 2px solid #dee2e6; + border-radius: 8px; + outline: none; + background: #fff; + color: #212529; + transition: none !important; +} + +.search-modal .search .search-input:focus { + border-color: #0050d0; + box-shadow: 0 0 0 4px rgba(0, 80, 208, 0.1); +} + +.search-modal .search .search-input::placeholder { + color: #adb5bd; +} + +/* Override search label styles */ +.search-modal .search .search-label { + position: absolute; + left: 0.875rem; + top: 50%; + transform: translateY(-50%); + z-index: 1; + color: #6c757d; + pointer-events: none; + padding: 0 !important; + transition: none !important; + height: auto !important; +} + +.search-modal .search .search-label .search-icon { + width: 1.25rem; + height: 1.25rem; +} + +.search-modal .search #search-results { + display: block !important; + flex: 1; + overflow-y: auto; + position: relative !important; + top: auto !important; + left: auto !important; + width: auto !important; + box-shadow: none !important; + border-radius: 0 !important; + background: transparent !important; +} + +/* Modal close button */ +.search-modal-close { + position: absolute; + top: 1.25rem; + right: 1.25rem; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: none; + background: transparent; + color: #6c757d; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s ease; + z-index: 10; +} + +.search-modal-close:hover { + background: #f8f9fa; + color: #212529; +} + +/* Dark mode */ +html.dark-mode .search-modal-trigger-wrapper { + background: transparent; +} + +html.dark-mode .search-modal-trigger { + background: #2d2d2d !important; + border-color: #404040 !important; + color: #e0e0e0 !important; +} + +html.dark-mode .search-modal-trigger:hover { + background: #3d3d3d !important; + border-color: #505050 !important; +} + +html.dark-mode .search-modal-shortcut { + background: #1a1a1a !important; + border-color: #404040 !important; + color: #a0a0a0 !important; +} + +html.dark-mode .search-modal-overlay { + background: rgba(0, 0, 0, 0.7) !important; +} + +html.dark-mode .search-modal { + background: #1a1a1a !important; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5) !important; +} + +html.dark-mode .search-modal .search .search-input { + background: #2d2d2d !important; + border-color: #404040 !important; + color: #e0e0e0 !important; +} + +html.dark-mode .search-modal .search .search-input:focus { + border-color: #7dd3fc !important; + box-shadow: 0 0 0 4px rgba(125, 211, 252, 0.1) !important; +} + +html.dark-mode .search-modal .search .search-input::placeholder { + color: #666 !important; +} + +html.dark-mode .search-modal .search .search-label { + color: #a0a0a0 !important; +} + +html.dark-mode .search-modal .search #search-results { + background: transparent !important; +} + +html.dark-mode .search-modal-close { + color: #a0a0a0 !important; +} + +html.dark-mode .search-modal-close:hover { + background: #2d2d2d !important; + color: #fff !important; +} + +/* Dark mode scrollbar styles */ +html.dark-mode { + /* WebKit browsers (Chrome, Safari, Edge) */ + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar-track { + background: #1a1a1a; + } + + ::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 5px; + } + + ::-webkit-scrollbar-thumb:hover { + background: #505050; + } + + ::-webkit-scrollbar-corner { + background: #1a1a1a; + } +} + +/* Firefox scrollbar styles */ +@supports (scrollbar-width: thin) { + html.dark-mode { + scrollbar-color: #404040 #1a1a1a; + scrollbar-width: thin; + } +} + +/* Responsive */ +@media (max-width: 640px) { + .search-modal { + top: 5%; + width: 95%; + max-height: 85vh; + } + + .search-modal-trigger { + padding: 0.375rem 0.75rem; + } + + .search-modal-shortcut { + display: none; + } +} + diff --git a/docs/assets/js/search-modal.js b/docs/assets/js/search-modal.js new file mode 100644 index 0000000..72f81e3 --- /dev/null +++ b/docs/assets/js/search-modal.js @@ -0,0 +1,230 @@ +/** + * Search Modal - VitePress style search overlay + * Moves original Just the Docs search component into modal + */ + +(function() { + 'use strict'; + + let modal = null; + let modalOverlay = null; + let originalSearchContainer = null; + let isOpen = false; + + // Create modal HTML structure + function createModal() { + // Overlay + modalOverlay = document.createElement('div'); + modalOverlay.className = 'search-modal-overlay'; + modalOverlay.setAttribute('aria-hidden', 'true'); + + // Modal container + modal = document.createElement('div'); + modal.className = 'search-modal'; + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-modal', 'true'); + modal.setAttribute('aria-label', 'Search'); + + // Close button + const closeButton = document.createElement('button'); + closeButton.className = 'search-modal-close'; + closeButton.setAttribute('aria-label', 'Close'); + closeButton.innerHTML = ` + + + `; + closeButton.addEventListener('click', closeModal); + + // Assemble modal + modal.appendChild(closeButton); + + // Add to document + document.body.appendChild(modalOverlay); + document.body.appendChild(modal); + + // Event listeners + modalOverlay.addEventListener('click', closeModal); + document.addEventListener('keydown', handleGlobalKeydown); + } + + // Move original search component to modal + function moveSearchToModal() { + const mainHeader = document.querySelector('#main-header'); + if (!mainHeader) return false; + + originalSearchContainer = mainHeader.querySelector('.search'); + if (!originalSearchContainer) return false; + + // Move search container to modal + modal.appendChild(originalSearchContainer); + + // Update input placeholder + const searchInput = document.getElementById('search-input'); + if (searchInput) { + searchInput.placeholder = 'Search twinBASIC Documentation'; + } + + // Force reflow to ensure styles apply + void modal.offsetHeight; + + console.log('Search moved to modal, modal children:', modal.children.length); + + return true; + } + + // Open modal + function openModal() { + // Ensure modal exists + if (!modal) { + createModal(); + } + + // Move search component to modal if not done + if (!modal.querySelector('.search')) { + if (!moveSearchToModal()) { + // Wait for search to be ready + const checkInterval = setInterval(() => { + if (moveSearchToModal()) { + clearInterval(checkInterval); + openModalNow(); + } + }, 200); + + // Timeout after 3 seconds + setTimeout(() => clearInterval(checkInterval), 3000); + return; + } + } + + openModalNow(); + } + + function openModalNow() { + if (!modal || !modalOverlay) { + console.error('Modal or overlay not found!'); + return; + } + + isOpen = true; + modalOverlay.classList.add('active'); + modal.classList.add('active'); + document.body.style.overflow = 'hidden'; + + // Focus on search input + setTimeout(() => { + const searchInput = document.getElementById('search-input'); + if (searchInput) { + searchInput.focus(); + } + }, 100); + } + + // Close modal + function closeModal() { + if (!modal) return; + + isOpen = false; + modalOverlay.classList.remove('active'); + modal.classList.remove('active'); + document.body.style.overflow = ''; + + // Clear search input + const searchInput = document.getElementById('search-input'); + if (searchInput) { + searchInput.value = ''; + const keyupEvent = new KeyboardEvent('keyup', { + bubbles: true, + cancelable: true, + keyCode: 65, + key: '' + }); + searchInput.dispatchEvent(keyupEvent); + } + } + + // Handle global keyboard events + function handleGlobalKeydown(e) { + // Cmd/Ctrl + K to open search + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + if (isOpen) { + closeModal(); + } else { + openModal(); + } + } + + // Escape to close + if (e.key === 'Escape' && isOpen) { + closeModal(); + } + } + + // Create search button in header + function createSearchButton() { + const mainHeader = document.querySelector('#main-header'); + if (!mainHeader) { + setTimeout(createSearchButton, 200); + return; + } + + // Check if button already exists + if (document.querySelector('.search-modal-trigger')) { + return; + } + + // Find original search container + const searchContainer = mainHeader.querySelector('.search'); + if (!searchContainer) { + setTimeout(createSearchButton, 200); + return; + } + + // Find nav element + const nav = mainHeader.querySelector('nav'); + if (!nav) { + setTimeout(createSearchButton, 200); + return; + } + + // Create search button wrapper + const buttonWrapper = document.createElement('div'); + buttonWrapper.className = 'search-modal-trigger-wrapper'; + + // Create search button + const searchButton = document.createElement('button'); + searchButton.className = 'search-modal-trigger'; + searchButton.setAttribute('aria-label', 'Search'); + searchButton.innerHTML = ` + + + `; + + // Add keyboard shortcut hint + const shortcut = document.createElement('span'); + shortcut.className = 'search-modal-shortcut'; + shortcut.textContent = '⌘K'; + searchButton.appendChild(shortcut); + + searchButton.addEventListener('click', openModal); + buttonWrapper.appendChild(searchButton); + + // Insert button wrapper after nav + nav.parentNode.insertBefore(buttonWrapper, nav.nextSibling); + } + + // Initialize + function init() { + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + setTimeout(createSearchButton, 100); + }); + } else { + setTimeout(createSearchButton, 100); + } + } + + init(); + +})(); diff --git a/docs/assets/js/toc.js b/docs/assets/js/toc.js new file mode 100644 index 0000000..2c7cde1 --- /dev/null +++ b/docs/assets/js/toc.js @@ -0,0 +1,176 @@ +/** + * Just the Docs - In-page Table of Contents Generator + * Generates a floating TOC based on page headings (H1-H4) + */ + +(function() { + 'use strict'; + + // Configuration + const config = { + minLevel: 2, // Minimum heading level to include (h2) + maxLevel: 4, // Maximum heading level to include (h4) + containerId: 'page-toc', + containerClass: 'page-toc', + headingClass: 'page-toc-heading', + listClass: 'page-toc-list', + itemClass: 'page-toc-item', + linkClass: 'page-toc-link', + activeClass: 'active' + }; + + // Create TOC container + function createTOCContainer() { + const container = document.createElement('div'); + container.id = config.containerId; + container.className = config.containerClass; + return container; + } + + // Generate TOC from headings + function generateTOC() { + const content = document.querySelector('.main-content'); + if (!content) return null; + + const headings = content.querySelectorAll('h2, h3, h4'); + if (headings.length < 2) return null; + + const container = createTOCContainer(); + + // Add heading + const heading = document.createElement('div'); + heading.className = config.headingClass; + heading.textContent = 'On this page'; + container.appendChild(heading); + + // Create list + const list = document.createElement('ul'); + list.className = config.listClass; + + let currentList = list; + let lastLevel = config.minLevel; + const listStack = [list]; + + headings.forEach((heading, index) => { + const level = parseInt(heading.tagName.charAt(1)); + + if (level < config.minLevel || level > config.maxLevel) return; + + // Ensure heading has an ID for linking + if (!heading.id) { + heading.id = heading.textContent + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^\w\-]/g, '') + .replace(/\-+/g, '-') + .replace(/^-+|-+$/g, '') + '-' + index; + } + + // Handle nested lists + if (level > lastLevel) { + const subList = document.createElement('ul'); + subList.className = config.listClass; + const lastItem = currentList.lastElementChild; + if (lastItem) { + lastItem.appendChild(subList); + currentList = subList; + listStack.push(subList); + } + } else if (level < lastLevel) { + while (listStack.length > 1) { + listStack.pop(); + currentList = listStack[listStack.length - 1]; + const stackTopLevel = getCurrentListLevel(currentList); + if (stackTopLevel <= level) break; + } + } + + // Create list item + const item = document.createElement('li'); + item.className = config.itemClass; + item.style.paddingLeft = ((level - config.minLevel) * 12) + 'px'; + + const link = document.createElement('a'); + link.className = config.linkClass; + link.href = '#' + heading.id; + link.textContent = heading.textContent; + link.dataset.target = heading.id; + + item.appendChild(link); + currentList.appendChild(item); + lastLevel = level; + }); + + container.appendChild(list); + return container; + } + + function getCurrentListLevel(list) { + let level = config.minLevel; + let parent = list.parentElement; + while (parent && parent.tagName !== 'DIV') { + if (parent.tagName === 'UL') level++; + parent = parent.parentElement; + } + return level; + } + + // Insert TOC into page + function insertTOC(toc) { + const mainContent = document.querySelector('.main-content'); + if (!mainContent) return; + + // Insert after the first h1 + const firstH1 = mainContent.querySelector('h1'); + if (firstH1) { + firstH1.parentNode.insertBefore(toc, firstH1.nextSibling); + } else { + mainContent.insertBefore(toc, mainContent.firstChild); + } + } + + // Highlight active heading on scroll + function setupScrollSpy() { + const headings = document.querySelectorAll('.main-content h2, .main-content h3, .main-content h4'); + const tocLinks = document.querySelectorAll(`.${config.linkClass}`); + + if (tocLinks.length === 0) return; + + const observerOptions = { + rootMargin: '-100px 0px -66%', + threshold: 0 + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const id = entry.target.id; + tocLinks.forEach(link => { + link.classList.remove(config.activeClass); + if (link.dataset.target === id) { + link.classList.add(config.activeClass); + } + }); + } + }); + }, observerOptions); + + headings.forEach(heading => observer.observe(heading)); + } + + // Initialize + function init() { + const toc = generateTOC(); + if (toc) { + insertTOC(toc); + setupScrollSpy(); + } + } + + // Run when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})();