Highly ambitious ATProtocol AppView service and sdks
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}