Highly ambitious ATProtocol AppView service and sdks
at main 179 lines 7.1 kB view raw
1import type { AuthenticatedUser } from "../../../routes/middleware.ts"; 2import { Layout } from "../../../shared/fragments/Layout.tsx"; 3import { Text } from "../../../shared/fragments/Text.tsx"; 4import { Breadcrumb } from "../../../shared/fragments/Breadcrumb.tsx"; 5 6interface DocItem { 7 slug: string; 8 title: string; 9 description: string; 10} 11 12interface DocCategory { 13 category: string; 14 docs: DocItem[]; 15} 16 17interface HeaderItem { 18 level: number; 19 text: string; 20 id: string; 21} 22 23interface DocsPageProps { 24 title: string; 25 content: string; 26 headers: HeaderItem[]; 27 docs: DocItem[]; 28 categories: DocCategory[]; 29 currentSlug: string; 30 currentUser?: AuthenticatedUser; 31} 32 33export function DocsPage({ 34 title, 35 content, 36 headers, 37 currentUser, 38}: DocsPageProps) { 39 return ( 40 <Layout title={`${title} - Slices`} currentUser={currentUser}> 41 <div className="py-8 px-4 max-w-6xl mx-auto relative"> 42 {/* Breadcrumb */} 43 <Breadcrumb 44 items={[{ label: "Documentation", href: "/docs" }, { label: title }]} 45 /> 46 47 {/* Two-column layout */} 48 <div className="flex gap-12"> 49 {/* Main Content */} 50 <main className="flex-1 min-w-0"> 51 <article className="prose prose-zinc dark:prose-invert max-w-none"> 52 <div 53 className="docs-content [&_pre]:overflow-x-auto [&_pre]:max-w-full [&_pre]:border [&_pre]:border-zinc-200 dark:[&_pre]:border-zinc-700 [&_pre]:rounded-lg [&_pre>code]:border-0 [&_:not(pre)>code]:bg-zinc-100 dark:[&_:not(pre)>code]:bg-zinc-800 [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:text-sm" 54 dangerouslySetInnerHTML={{ __html: content }} 55 /> 56 </article> 57 </main> 58 59 {/* Right Sidebar - Table of Contents */} 60 {headers.length > 0 && ( 61 <aside className="hidden lg:flex w-64 flex-shrink-0 relative"> 62 <div className="sticky top-1/2 -translate-y-1/2 w-64 max-h-[60vh] overflow-y-auto bg-zinc-50 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4 shadow-sm"> 63 <Text 64 as="h3" 65 size="sm" 66 className="font-semibold mb-4 text-zinc-900 dark:text-white" 67 > 68 On This Page 69 </Text> 70 <nav> 71 <ul className="space-y-1 text-sm" id="toc-nav"> 72 {headers.map((header) => ( 73 <li key={header.id}> 74 <a 75 href={`#${header.id}`} 76 data-target={header.id} 77 /* @ts-ignore - Hyperscript attribute */ 78 _="on click call updateActiveTocLink(me) then on load call updateActiveTocOnScroll()" 79 className={`block py-1.5 px-3 -mx-3 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800/50 ${ 80 header.level === 1 81 ? "text-zinc-900 dark:text-white font-medium" 82 : header.level === 2 83 ? "text-zinc-700 dark:text-zinc-300" 84 : "text-zinc-600 dark:text-zinc-400 ml-2" 85 } [&.active]:bg-blue-50 dark:[&.active]:bg-blue-950/50 [&.active]:text-blue-600 dark:[&.active]:text-blue-400`} 86 > 87 {header.text} 88 </a> 89 </li> 90 ))} 91 </ul> 92 </nav> 93 </div> 94 </aside> 95 )} 96 </div> 97 </div> 98 99 {/* Add scroll tracking script */} 100 <script 101 dangerouslySetInnerHTML={{ 102 __html: ` 103 function updateActiveTocLink(clickedLink) { 104 // Remove active from all TOC links 105 document.querySelectorAll('#toc-nav a').forEach(link => { 106 link.classList.remove('active'); 107 }); 108 // Add active to clicked link 109 clickedLink.classList.add('active'); 110 } 111 112 function updateActiveTocOnScroll() { 113 const headers = Array.from(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')); 114 const tocLinks = document.querySelectorAll('#toc-nav a[data-target]'); 115 116 function updateActiveHeader() { 117 let activeHeader = null; 118 119 // Find the header that's currently in view 120 for (let i = headers.length - 1; i >= 0; i--) { 121 const header = headers[i]; 122 const rect = header.getBoundingClientRect(); 123 124 // If header is above the top quarter of the viewport, it's the active one 125 if (rect.top <= window.innerHeight / 4) { 126 activeHeader = header; 127 break; 128 } 129 } 130 131 // Update TOC links 132 tocLinks.forEach(link => { 133 link.classList.remove('active'); 134 if (activeHeader && link.dataset.target === activeHeader.id) { 135 link.classList.add('active'); 136 137 // Scroll the active link into view within the TOC container 138 const tocNav = document.querySelector('#toc-nav'); 139 const tocContainer = tocNav.closest('.overflow-y-auto'); 140 141 if (tocContainer && tocContainer.scrollHeight > tocContainer.clientHeight) { 142 // Get the position of the active link relative to the scrollable container 143 const containerRect = tocContainer.getBoundingClientRect(); 144 const linkRect = link.getBoundingClientRect(); 145 146 const isAbove = linkRect.top < containerRect.top + 40; // 40px buffer from top 147 const isBelow = linkRect.bottom > containerRect.bottom - 40; // 40px buffer from bottom 148 149 if (isAbove || isBelow) { 150 // Calculate scroll position to center the link 151 const linkOffsetTop = link.offsetTop; 152 const containerHeight = tocContainer.clientHeight; 153 const targetScrollTop = linkOffsetTop - (containerHeight / 2); 154 155 tocContainer.scrollTo({ 156 top: Math.max(0, targetScrollTop), 157 behavior: 'smooth' 158 }); 159 } 160 } 161 } 162 }); 163 } 164 165 // Update on scroll 166 window.addEventListener('scroll', updateActiveHeader); 167 168 // Update on page load 169 updateActiveHeader(); 170 } 171 172 // Initialize scroll tracking when page loads 173 document.addEventListener('DOMContentLoaded', updateActiveTocOnScroll); 174 `, 175 }} 176 /> 177 </Layout> 178 ); 179}