main
svelte 108 lines 3.35 KB
Raw
1 <script lang="ts">
2 import { page } from '$app/stores';
3 import { base } from '$app/paths';
4 import type { NavSection } from '$lib/nav';
5
6 let { sections }: { sections: NavSection[] } = $props();
7
8 const currentPath = $derived($page.url.pathname);
9
10 // Track user-toggled state per section. null means "not manually toggled"
11 let userToggles: Record<number, boolean | null> = $state({});
12
13 function isItemActive(href: string): boolean {
14 return (
15 currentPath === `${base}${href}/` ||
16 currentPath === `${base}${href}` ||
17 currentPath.startsWith(`${base}${href}/`)
18 );
19 }
20
21 function sectionHasActiveChild(sectionIndex: number): boolean {
22 return sections[sectionIndex].items.some((item) => isItemActive(item.href));
23 }
24
25 function isSectionOpen(index: number): boolean {
26 const userToggle = userToggles[index];
27 if (userToggle !== null && userToggle !== undefined) {
28 return userToggle;
29 }
30 // Auto-open if section has active child or defaultOpen
31 return sectionHasActiveChild(index) || (sections[index].defaultOpen ?? false);
32 }
33
34 function toggleSection(index: number) {
35 const currentlyOpen = isSectionOpen(index);
36 userToggles[index] = !currentlyOpen;
37 }
38
39 // When the route changes to a page within a section, force that section open
40 $effect(() => {
41 // Access currentPath to create the dependency
42 void currentPath;
43 for (let i = 0; i < sections.length; i++) {
44 if (sectionHasActiveChild(i)) {
45 userToggles[i] = true;
46 }
47 }
48 });
49 </script>
50
51 <nav class="space-y-1" aria-label="Documentation">
52 {#each sections as section, index (section.title)}
53 {@const isOpen = isSectionOpen(index)}
54 <div>
55 <button
56 type="button"
57 class="flex w-full cursor-pointer items-center justify-between rounded-md px-1 py-2 font-display text-xs font-semibold tracking-wider text-text-muted uppercase transition-colors hover:text-foreground"
58 aria-expanded={isOpen}
59 onclick={() => toggleSection(index)}
60 >
61 <span class="flex items-center gap-2">
62 {section.title}
63 {#if section.badge}
64 <span
65 class="rounded-full border border-amber-400/25 bg-amber-400/15 px-1.5 py-0.5 text-[10px] font-medium tracking-normal normal-case text-amber-600 dark:text-amber-400"
66 >
67 {section.badge}
68 </span>
69 {/if}
70 </span>
71 <svg
72 class="h-3.5 w-3.5 shrink-0 transition-transform duration-200 {isOpen
73 ? 'rotate-90'
74 : 'rotate-0'}"
75 viewBox="0 0 16 16"
76 fill="currentColor"
77 aria-hidden="true"
78 >
79 <path
80 d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z"
81 />
82 </svg>
83 </button>
84 <div
85 class="grid transition-[grid-template-rows] duration-200 ease-in-out {isOpen
86 ? 'grid-rows-[1fr]'
87 : 'grid-rows-[0fr]'}"
88 >
89 <ul class="overflow-hidden">
90 {#each section.items as item (item.href)}
91 {@const isActive = isItemActive(item.href)}
92 <li>
93 <a
94 href="{base}{item.href}"
95 class="block rounded-lg px-3 py-2 text-sm transition-colors {isActive
96 ? 'bg-primary/10 font-medium text-primary dark:bg-primary-light/15 dark:text-primary-light'
97 : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/6'}"
98 aria-current={isActive ? 'page' : undefined}
99 >
100 {item.title}
101 </a>
102 </li>
103 {/each}
104 </ul>
105 </div>
106 </div>
107 {/each}
108 </nav>