Files
robonen 8adc2522c6 docs: site WIP, extractor type cleanup, tests preset; add broadcastedRef
Type the docs extractor's package.json parsing as unknown; comment the Vite
plugin version-skew cast; wire the tests preset; site/architecture WIP.
2026-06-15 16:55:22 +07:00

298 lines
14 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">const { getGroupedPackages, getPackage, getIntro, getDocSections, getComponentGroups } = useDocs();
const groups = getGroupedPackages();
const route = useRoute();
const isSidebarOpen = ref(false);
const currentPackageSlug = computed(() => {
const param = route.params.package;
return typeof param === 'string' ? param : undefined;
});
const currentPackage = computed(() =>
currentPackageSlug.value ? getPackage(currentPackageSlug.value) : undefined,
);
function isActive(pkgSlug: string, slug: string) {
return route.path === `/${pkgSlug}/${slug}`;
}
// ── Category tree: collapsed by default, filterable ───────────────────────
const navQuery = ref('');
const openCategories = ref(new Set<string>());
function toggleCategory(slug: string) {
if (openCategories.value.has(slug))
openCategories.value.delete(slug);
else
openCategories.value.add(slug);
openCategories.value = new Set(openCategories.value);
}
// Auto-open the category that contains the current page
watch([currentPackage, () => route.path], () => {
const pkg = currentPackage.value;
if (!pkg || pkg.kind !== 'api') return;
for (const category of pkg.categories) {
if (category.items.some(item => isActive(pkg.slug, item.slug)))
openCategories.value.add(category.slug);
}
openCategories.value = new Set(openCategories.value);
}, { immediate: true });
// Reset the filter when navigating to another package
watch(currentPackageSlug, () => {
navQuery.value = '';
});
const visibleCategories = computed(() => {
const pkg = currentPackage.value;
if (!pkg || pkg.kind !== 'api') return [];
const query = navQuery.value.trim().toLowerCase();
if (!query) return pkg.categories;
return pkg.categories
.map(category => ({
...category,
items: category.items.filter(item => item.name.toLowerCase().includes(query)),
}))
.filter(category => category.items.length > 0);
});
function isCategoryOpen(slug: string) {
// Filtering expands every matching category
return navQuery.value.trim() !== '' || openCategories.value.has(slug);
}
watch(() => route.path, () => {
isSidebarOpen.value = false;
});
</script>
<template>
<div class="min-h-screen">
<!-- Header -->
<header class="sticky top-0 z-50 border-b border-border backdrop-blur-md" style="background-color: var(--header-bg)">
<div class="mx-auto max-w-352 flex items-center gap-3 px-4 h-14 sm:px-6">
<button
type="button"
class="lg:hidden inline-flex items-center justify-center w-9 h-9 -ml-1.5 rounded-lg text-fg-muted hover:text-fg hover:bg-bg-inset"
aria-label="Toggle navigation"
@click="isSidebarOpen = !isSidebarOpen"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
<NuxtLink to="/" class="group flex items-center gap-2.5 mr-auto">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-md bg-accent text-accent-fg font-mono text-[13px] font-semibold leading-none select-none">
</span>
<span class="hidden sm:flex items-baseline font-mono text-[13.5px] tracking-tight">
<span class="text-fg-subtle">~/</span><span class="text-fg font-medium">robonen</span><span class="text-fg-subtle">/</span><span class="text-accent-text font-medium">tools</span>
<span class="ml-1 inline-block w-1.75 h-3.75 translate-y-0.5 bg-accent opacity-0 group-hover:opacity-80 group-hover:animate-pulse" />
</span>
</NuxtLink>
<DocsSearch />
<DocsThemeToggle />
<a
href="https://github.com/robonen/tools"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-fg-muted hover:text-fg hover:bg-bg-inset transition-colors"
aria-label="GitHub"
>
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
</div>
</header>
<div class="mx-auto max-w-352 w-full flex px-4 sm:px-6">
<!-- Sidebar -->
<aside
:class="[
'fixed inset-y-0 left-0 z-40 w-72 bg-bg border-r border-border pt-14 transform transition-transform lg:sticky lg:top-14 lg:z-auto lg:h-[calc(100vh-3.5rem)] lg:w-64 lg:shrink-0 lg:translate-x-0 lg:pt-0 lg:border-r-0 lg:bg-transparent',
isSidebarOpen ? 'translate-x-0' : '-translate-x-full',
]"
>
<nav class="h-full overflow-y-auto py-8 px-4 lg:pr-6 lg:pl-0 overscroll-contain">
<div v-for="grp in groups" :key="grp.group" class="mb-7">
<div class="comment-label mb-2 px-2">{{ grp.label.toLowerCase() }}</div>
<ul class="space-y-0.5">
<li v-for="pkg in grp.packages" :key="pkg.slug">
<NuxtLink
:to="`/${pkg.slug}`"
:class="[
'flex items-center justify-between py-1.5 px-2 rounded-md text-sm transition-colors',
currentPackageSlug === pkg.slug
? 'text-fg font-medium bg-bg-inset'
: 'text-fg-muted hover:text-fg hover:bg-bg-inset',
]"
>
<span class="font-mono text-[13px]">{{ pkg.name.replace('@robonen/', '') }}</span>
<span class="text-[10px] font-mono text-fg-subtle">{{ pkg.kind === 'api' ? 'api' : pkg.kind === 'components' ? 'ui' : 'guide' }}</span>
</NuxtLink>
<!-- Expanded tree for the current package -->
<div v-if="currentPackageSlug === pkg.slug && currentPackage" class="mt-1.5 mb-3 ml-2.5 pl-2.5 border-l border-border">
<!-- Quick filter the tree below collapses to matches -->
<div v-if="currentPackage.kind === 'api'" class="relative mb-2 mt-1">
<span class="absolute left-2 top-1/2 -translate-y-1/2 font-mono text-[11px] text-accent-text select-none"></span>
<input
v-model="navQuery"
type="text"
placeholder="filter…"
class="w-full h-7 pl-6 pr-2 font-mono text-[12px] rounded-md bg-bg-subtle border border-border text-fg placeholder:text-fg-subtle focus:outline-none focus:border-border-strong transition-colors"
>
</div>
<!-- Hand-authored guide sections (intro + prose pages) -->
<div v-if="currentPackage.docs.length && !navQuery" class="mb-2">
<div class="comment-label py-1 px-1">guide</div>
<ul>
<li v-if="getIntro(currentPackage)">
<NuxtLink
:to="`/${pkg.slug}`"
:class="[
'block py-1 px-2 text-[13px] rounded-md transition-colors truncate',
route.path === `/${pkg.slug}`
? 'text-accent-text font-medium'
: 'text-fg-muted hover:text-fg hover:bg-bg-inset',
]"
>
Introduction
</NuxtLink>
</li>
<li v-for="s in getDocSections(currentPackage)" :key="s.slug">
<NuxtLink
:to="`/${pkg.slug}/${s.slug}`"
:class="[
'block py-1 px-2 text-[13px] rounded-md transition-colors truncate',
isActive(pkg.slug, s.slug)
? 'text-accent-text font-medium'
: 'text-fg-muted hover:text-fg hover:bg-bg-inset',
]"
>
{{ s.title }}
</NuxtLink>
</li>
</ul>
</div>
<!-- api: collapsible categories -->
<template v-if="currentPackage.kind === 'api'">
<div v-if="navQuery && visibleCategories.length === 0" class="py-2 px-1 font-mono text-[11px] text-fg-subtle">
no matches
</div>
<div v-for="cat in visibleCategories" :key="cat.slug" class="mb-0.5">
<button
type="button"
class="w-full flex items-center gap-1.5 py-1 px-1 rounded-md cursor-pointer group/cat"
@click="toggleCategory(cat.slug)"
>
<svg
xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
:class="[
'shrink-0 text-fg-subtle transition-transform duration-150',
isCategoryOpen(cat.slug) ? 'rotate-90' : '',
]"
>
<polyline points="9 18 15 12 9 6" />
</svg>
<span class="comment-label group-hover/cat:text-fg-muted transition-colors">{{ cat.name.toLowerCase() }}</span>
<span class="ml-auto font-mono text-[10px] text-fg-subtle tabular-nums">{{ cat.items.length }}</span>
</button>
<ul v-if="isCategoryOpen(cat.slug)" class="mb-1.5">
<li v-for="item in cat.items" :key="item.slug">
<NuxtLink
:to="`/${pkg.slug}/${item.slug}`"
:class="[
'flex items-center gap-1.5 py-0.75 px-2 text-[13px] rounded-md font-mono transition-colors',
isActive(pkg.slug, item.slug)
? 'text-accent-text font-medium'
: 'text-fg-muted hover:text-fg hover:bg-bg-inset',
]"
>
<span
:class="[
'shrink-0 text-[10px] select-none transition-opacity',
isActive(pkg.slug, item.slug) ? 'opacity-100 text-accent-text' : 'opacity-0',
]"
></span>
<span class="truncate">{{ item.name }}</span>
</NuxtLink>
</li>
</ul>
</div>
</template>
<!-- components: grouped by functional category -->
<template v-else-if="currentPackage.kind === 'components'">
<div v-for="group in getComponentGroups(currentPackage)" :key="group.name" class="mb-2">
<div class="comment-label py-1 px-1">{{ group.name.toLowerCase() }}</div>
<ul>
<li v-for="c in group.components" :key="c.slug">
<NuxtLink
:to="`/${pkg.slug}/${c.slug}`"
:class="[
'block py-1 px-2 text-[13px] rounded-md transition-colors truncate',
isActive(pkg.slug, c.slug)
? 'text-accent-text font-medium'
: 'text-fg-muted hover:text-fg hover:bg-bg-inset',
]"
>
{{ c.name }}
</NuxtLink>
</li>
</ul>
</div>
</template>
<!-- guide -->
<ul v-else>
<li v-for="s in currentPackage.sections" :key="s.slug">
<NuxtLink
:to="`/${pkg.slug}/${s.slug}`"
:class="[
'block py-1 px-2 text-[13px] rounded-md transition-colors truncate',
isActive(pkg.slug, s.slug)
? 'text-accent-text font-medium'
: 'text-fg-muted hover:text-fg hover:bg-bg-inset',
]"
>
{{ s.title }}
</NuxtLink>
</li>
</ul>
</div>
</li>
</ul>
</div>
</nav>
</aside>
<!-- Mobile overlay -->
<div v-if="isSidebarOpen" class="fixed inset-0 z-30 bg-black/30 lg:hidden" @click="isSidebarOpen = false" />
<!-- Main content -->
<main class="flex-1 min-w-0 py-10 lg:pl-10">
<slot />
</main>
</div>
</div>
</template>