Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
96 changes: 96 additions & 0 deletions site/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build": "astro build && npx pagefind --site dist",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/mdx": "^5.0.2",
"@astrojs/sitemap": "^3.7.1",
"@tailwindcss/vite": "^4.2.2",
"astro": "^6.0.8",
"pagefind": "^1.4.0",
"tailwindcss": "^4.2.2"
}
}
137 changes: 137 additions & 0 deletions site/src/components/DocsSearch.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
---
<div class="relative" id="search-container">
<button
id="search-trigger"
class="flex items-center gap-2 px-3 py-1.5 text-xs text-on-surface-variant border border-outline-variant bg-surface-container-low hover:border-outline transition-colors font-mono w-full lg:w-48"
type="button"
>
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<span class="flex-1 text-left">Search docs...</span>
<kbd class="hidden lg:inline text-[10px] text-on-surface-variant/60 border border-outline-variant/50 px-1 rounded-sm">⌘K</kbd>
</button>
</div>

<!-- Search modal -->
<div id="search-modal" class="fixed inset-0 z-[100] hidden">
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm" id="search-backdrop"></div>
<div class="fixed top-[15%] left-1/2 -translate-x-1/2 w-full max-w-xl bg-surface-container border border-outline-variant shadow-2xl max-h-[60vh] flex flex-col">
<div class="flex items-center gap-3 px-4 py-3 border-b border-outline-variant">
<svg class="w-4 h-4 text-on-surface-variant shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
id="search-input"
type="text"
placeholder="Search documentation..."
class="flex-1 bg-transparent text-on-surface text-sm outline-none placeholder:text-on-surface-variant/50 font-body"
autocomplete="off"
/>
<kbd class="text-[10px] text-on-surface-variant/60 border border-outline-variant/50 px-1.5 py-0.5 rounded-sm">Esc</kbd>
</div>
Comment thread
michaelmcnees marked this conversation as resolved.
Outdated
<div id="search-results" class="overflow-y-auto p-2 text-sm">
<p class="px-3 py-4 text-on-surface-variant/60 text-center text-xs">Type to search...</p>
</div>
Comment thread
michaelmcnees marked this conversation as resolved.
Outdated
</div>
</div>

<script is:inline>
function initSearch() {
const trigger = document.getElementById('search-trigger');
const modal = document.getElementById('search-modal');
const backdrop = document.getElementById('search-backdrop');
const input = document.getElementById('search-input');
const results = document.getElementById('search-results');
if (!trigger || !modal || !input || !results) return;

let pagefind = null;

async function loadPagefind() {
if (pagefind) return pagefind;
try {
pagefind = await import('/pagefind/pagefind.js');
await pagefind.init();
} catch (e) {
console.warn('Pagefind not available (run build first):', e);
pagefind = null;
}
return pagefind;
}

function openSearch() {
modal.classList.remove('hidden');
input.value = '';
input.focus();
results.innerHTML = '<p class="px-3 py-4 text-on-surface-variant/60 text-center text-xs">Type to search...</p>';
}

function closeSearch() {
modal.classList.add('hidden');
}

trigger.onclick = openSearch;
backdrop.onclick = closeSearch;

// Cmd+K / Ctrl+K shortcut
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
if (modal.classList.contains('hidden')) openSearch();
else closeSearch();
}
if (e.key === 'Escape') closeSearch();
});
Comment thread
michaelmcnees marked this conversation as resolved.
Outdated

let debounceTimer;
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const query = input.value.trim();
if (!query) {
results.innerHTML = '<p class="px-3 py-4 text-on-surface-variant/60 text-center text-xs">Type to search...</p>';
return;
}

const pf = await loadPagefind();
if (!pf) {
results.innerHTML = '<p class="px-3 py-4 text-on-surface-variant/60 text-center text-xs">Search unavailable (build required)</p>';
return;
}

const search = await pf.search(query);
if (search.results.length === 0) {
results.innerHTML = '<p class="px-3 py-4 text-on-surface-variant/60 text-center text-xs">No results for &quot;' + query.replace(/[<>"&]/g, '') + '&quot;</p>';
return;
}

const items = await Promise.all(search.results.slice(0, 8).map(r => r.data()));
const container = document.createElement('div');
items.forEach(item => {
const link = document.createElement('a');
link.href = item.url;
link.className = 'block px-3 py-2 hover:bg-surface-container-high transition-colors group';

const title = document.createElement('div');
title.className = 'font-medium text-on-surface text-sm group-hover:text-primary transition-colors';
title.textContent = item.meta?.title || item.url;
link.appendChild(title);

const excerpt = document.createElement('div');
excerpt.className = 'text-xs text-on-surface-variant mt-0.5 line-clamp-2';
// Pagefind excerpt contains <mark> tags from our own build index — safe to render
excerpt.innerHTML = item.excerpt;
Comment thread
michaelmcnees marked this conversation as resolved.
Outdated
link.appendChild(excerpt);

link.addEventListener('click', closeSearch);
container.appendChild(link);
});

results.replaceChildren(...container.childNodes);
}, 200);
});
}
initSearch();
document.addEventListener('astro:after-swap', initSearch);
</script>
43 changes: 27 additions & 16 deletions site/src/components/DocsSidebar.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
import { docsNav } from '../data/docs-nav';
import type { NavItem } from '../data/docs-nav';
import DocsSearch from './DocsSearch.astro';

interface Props {
currentPath: string;
Expand All @@ -20,6 +21,9 @@ function isParentActive(item: NavItem): boolean {

<aside id="docs-sidebar" class="docs-sidebar hidden lg:block w-60 shrink-0">
<div class="sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto py-8 pr-4 pl-2">
<div class="mb-6">
<DocsSearch />
</div>
<nav aria-label="Documentation sidebar">
{docsNav.map((group) => (
<div class="mb-8">
Expand Down Expand Up @@ -93,7 +97,7 @@ function isParentActive(item: NavItem): boolean {
<div id="sidebar-overlay" class="fixed inset-0 z-40 bg-black/50 hidden lg:hidden" aria-hidden="true"></div>

<!-- Mobile sidebar drawer -->
<aside id="docs-sidebar-mobile" class="fixed top-16 left-0 bottom-0 z-50 w-72 bg-surface-container-lowest border-r border-outline-variant transform -translate-x-full transition-transform duration-200 lg:hidden overflow-y-auto">
<aside id="docs-sidebar-mobile" class="fixed top-16 left-0 bottom-14 md:bottom-0 z-50 w-72 bg-surface-container-lowest border-r border-outline-variant transform -translate-x-full transition-transform duration-200 lg:hidden overflow-y-auto">
<nav class="py-6 px-4" aria-label="Documentation sidebar mobile">
{docsNav.map((group) => (
<div class="mb-8">
Expand All @@ -111,7 +115,7 @@ function isParentActive(item: NavItem): boolean {
<a
href={item.href}
class:list={[
'flex items-center justify-between px-3 py-1.5 text-[13px] transition-colors',
'flex items-center justify-between px-3 py-2 text-sm transition-colors',
active
? 'text-primary font-medium border-l-2 border-primary bg-primary/5'
: 'text-on-surface-variant hover:text-on-surface border-l-2 border-transparent hover:border-outline-variant',
Expand Down Expand Up @@ -140,7 +144,7 @@ function isParentActive(item: NavItem): boolean {
<a
href={child.href}
class:list={[
'block pl-6 pr-3 py-1 text-[12px] transition-colors',
'block pl-6 pr-3 py-1.5 text-[13px] transition-colors',
childActive
? 'text-primary font-medium border-l-2 border-primary bg-primary/5'
: 'text-on-surface-variant/80 hover:text-on-surface border-l-2 border-outline-variant/30 hover:border-outline-variant',
Expand All @@ -159,32 +163,39 @@ function isParentActive(item: NavItem): boolean {
</ul>
</div>
))}

<!-- Site navigation -->
<div class="mt-4 pt-4 border-t border-outline-variant">
<h4 class="text-[10px] font-headline font-bold uppercase tracking-[0.15em] text-on-surface-variant/60 mb-3 px-3">
Site
</h4>
<ul>
<li>
<a href="/" class="flex items-center gap-2 px-3 py-2 text-sm text-on-surface-variant hover:text-on-surface transition-colors">
Home
</a>
</li>
<li>
<a href="https://github.com/dvflw/mantle" class="flex items-center gap-2 px-3 py-2 text-sm text-on-surface-variant hover:text-on-surface transition-colors">
GitHub
</a>
Comment thread
michaelmcnees marked this conversation as resolved.
</li>
</ul>
</div>
</nav>
</aside>

<script is:inline>
function initSidebar() {
const toggleBtn = document.getElementById('sidebar-toggle');
const mobileSidebar = document.getElementById('docs-sidebar-mobile');
const overlay = document.getElementById('sidebar-overlay');
if (!toggleBtn || !mobileSidebar || !overlay) return;
if (!mobileSidebar || !overlay) return;

function openSidebar() {
mobileSidebar.classList.remove('-translate-x-full');
overlay.classList.remove('hidden');
}
function closeSidebar() {
mobileSidebar.classList.add('-translate-x-full');
overlay.classList.add('hidden');
}

const newBtn = toggleBtn.cloneNode(true);
toggleBtn.parentNode?.replaceChild(newBtn, toggleBtn);
newBtn.addEventListener('click', () => {
const isOpen = !mobileSidebar.classList.contains('-translate-x-full');
if (isOpen) closeSidebar(); else openSidebar();
});

const newOverlay = overlay.cloneNode(true);
overlay.parentNode?.replaceChild(newOverlay, overlay);
newOverlay.addEventListener('click', closeSidebar);
Comment thread
michaelmcnees marked this conversation as resolved.
Outdated
Expand Down
Loading
Loading