Skip to content

Commit 91e3a13

Browse files
committed
feat(api-docs): add scroll-to-top button and improve navigation
- Add floating scroll-to-top button that appears when scrolling down - Make TOC category titles clickable links to their sections - Remove sticky positioning from package header for better scroll behavior - Adjust scroll-margin-top values for consistent anchor positioning
1 parent e1b5d05 commit 91e3a13

4 files changed

Lines changed: 123 additions & 15 deletions

File tree

static/style.css

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,19 +1288,18 @@ body:has(.api-layout) .global-header {
12881288

12891289
/* Package Header */
12901290
.package-header {
1291-
position: sticky;
1292-
top: 0;
1291+
position: relative;
12931292
display: flex;
12941293
align-items: center;
12951294
justify-content: space-between;
12961295
flex-wrap: wrap;
12971296
gap: 1rem;
12981297
margin: -2.5rem -2.5rem 1.5rem -2.5rem;
12991298
padding: 1.5rem 2.5rem;
1299+
padding-bottom: 2.5rem;
13001300
background: var(--bg-card);
13011301
border-bottom: 1px solid var(--border);
13021302
border-radius: 16px 16px 0 0;
1303-
z-index: 100;
13041303
}
13051304

13061305
.package-header h1 {
@@ -1569,14 +1568,15 @@ body:has(.api-layout) .global-header {
15691568
font-size: 1.5rem;
15701569
color: var(--text-heading);
15711570
margin: 0 0 1.5rem;
1571+
scroll-margin-top: 6rem;
15721572
}
15731573

15741574
/* API Item (Function, Class, Interface, etc.) */
15751575
.api-item {
15761576
margin-bottom: 2.5rem;
15771577
padding-bottom: 2rem;
15781578
border-bottom: 1px solid var(--border);
1579-
scroll-margin-top: 7rem;
1579+
scroll-margin-top: 6rem;
15801580
}
15811581

15821582
.api-item:last-child {
@@ -1911,7 +1911,7 @@ body:has(.api-layout) .global-header {
19111911
.property-item {
19121912
padding: 0.75rem 0;
19131913
border-bottom: 1px solid var(--border);
1914-
scroll-margin-top: 7rem;
1914+
scroll-margin-top: 6rem;
19151915
}
19161916

19171917
.property-item:last-child {
@@ -2235,7 +2235,7 @@ body:has(.api-layout) .global-header {
22352235
margin-bottom: 1rem;
22362236
padding-bottom: 1rem;
22372237
border-bottom: 1px dashed var(--border);
2238-
scroll-margin-top: 7rem;
2238+
scroll-margin-top: 6rem;
22392239
}
22402240

22412241
.method-item:last-child {
@@ -2284,6 +2284,16 @@ body:has(.api-layout) .global-header {
22842284
font-weight: 600;
22852285
}
22862286

2287+
.toc-group h4 a {
2288+
color: inherit;
2289+
text-decoration: none;
2290+
transition: color 0.15s;
2291+
}
2292+
2293+
.toc-group h4 a:hover {
2294+
color: var(--accent);
2295+
}
2296+
22872297
.toc-group ul {
22882298
list-style: none;
22892299
margin: 0;
@@ -2964,3 +2974,58 @@ body:has(.api-layout) .global-header {
29642974
#search-container .pagefind-ui__result-nested .pagefind-ui__result-excerpt {
29652975
-webkit-line-clamp: 1;
29662976
}
2977+
2978+
/* ============================================================================
2979+
Scroll to Top Button
2980+
============================================================================ */
2981+
2982+
.scroll-to-top {
2983+
position: fixed;
2984+
bottom: 2rem;
2985+
right: 2rem;
2986+
width: 48px;
2987+
height: 48px;
2988+
display: flex;
2989+
align-items: center;
2990+
justify-content: center;
2991+
background: var(--accent);
2992+
color: white;
2993+
border: none;
2994+
border-radius: 50%;
2995+
cursor: pointer;
2996+
opacity: 0;
2997+
visibility: hidden;
2998+
transform: translateY(10px);
2999+
transition: opacity 0.2s, visibility 0.2s, transform 0.2s, background 0.2s;
3000+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
3001+
z-index: 1000;
3002+
}
3003+
3004+
.scroll-to-top:hover {
3005+
background: var(--accent-hover);
3006+
transform: translateY(-2px);
3007+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
3008+
}
3009+
3010+
.scroll-to-top.visible {
3011+
opacity: 1;
3012+
visibility: visible;
3013+
transform: translateY(0);
3014+
}
3015+
3016+
.scroll-to-top i {
3017+
font-size: 1.5rem;
3018+
}
3019+
3020+
@media (max-width: 768px) {
3021+
.scroll-to-top {
3022+
bottom: 1rem;
3023+
right: 1rem;
3024+
width: 40px;
3025+
height: 40px;
3026+
}
3027+
3028+
.scroll-to-top i {
3029+
font-size: 1.25rem;
3030+
}
3031+
}

templates/Layout.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,10 @@ export function Layout(
122122
<Header showLogo={showLogo} />
123123
{children}
124124
<SearchModal />
125+
<ScrollToTop />
125126
<script src={`${basePath}/pagefind/pagefind-ui.js`} />
126127
<script dangerouslySetInnerHTML={{ __html: searchScript }} />
128+
<script dangerouslySetInnerHTML={{ __html: scrollToTopScript }} />
127129
</body>
128130
</html>
129131
);
@@ -154,6 +156,35 @@ function SearchModal() {
154156
);
155157
}
156158

159+
function ScrollToTop() {
160+
return (
161+
<button
162+
type="button"
163+
id="scroll-to-top"
164+
class="scroll-to-top"
165+
onclick="scrollToTop()"
166+
aria-label="Scroll to top"
167+
>
168+
<i class="ti ti-chevron-up" />
169+
</button>
170+
);
171+
}
172+
173+
const scrollToTopScript = `
174+
function scrollToTop() {
175+
window.scrollTo({ top: 0, behavior: 'smooth' });
176+
}
177+
178+
window.addEventListener('scroll', () => {
179+
const btn = document.getElementById('scroll-to-top');
180+
if (window.scrollY > 300) {
181+
btn.classList.add('visible');
182+
} else {
183+
btn.classList.remove('visible');
184+
}
185+
});
186+
`;
187+
157188
const searchScript = `
158189
let searchInitialized = false;
159190

templates/api/ApiPage.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export async function PackagePage({ packageName }: PackagePageProps) {
237237
{/* Render each kind group */}
238238
{orderedKinds.map((kind) => (
239239
<section key={kind} class="api-kind-section">
240-
<h2>{kindToTitle(kind)}</h2>
240+
<h2 id={kindInfoMap[kind]?.id}>{kindToTitle(kind)}</h2>
241241
{grouped[kind].map((node) => {
242242
const key = `${node.kind}:${node.name}`;
243243
const overloads = overloadMap.get(key);
@@ -279,13 +279,21 @@ interface KindInfo {
279279
icon: string;
280280
}
281281

282-
const kindInfoMap: Record<string, KindInfo> = {
283-
class: { title: "Classes", icon: "ti-box" },
284-
interface: { title: "Interfaces", icon: "ti-puzzle" },
285-
function: { title: "Functions", icon: "ti-code" },
286-
typeAlias: { title: "Type Aliases", icon: "ti-tag" },
287-
variable: { title: "Variables", icon: "ti-variable" },
288-
enum: { title: "Enums", icon: "ti-list" },
282+
const kindInfoMap: Record<string, KindInfo & { id: string }> = {
283+
class: { title: "Classes", icon: "ti-box", id: "category-classes" },
284+
interface: {
285+
title: "Interfaces",
286+
icon: "ti-puzzle",
287+
id: "category-interfaces",
288+
},
289+
function: { title: "Functions", icon: "ti-code", id: "category-functions" },
290+
typeAlias: { title: "Types", icon: "ti-tag", id: "category-types" },
291+
variable: {
292+
title: "Variables",
293+
icon: "ti-variable",
294+
id: "category-variables",
295+
},
296+
enum: { title: "Enums", icon: "ti-list", id: "category-enums" },
289297
};
290298

291299
function kindToTitle(kind: string) {

templates/api/components.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1453,11 +1453,15 @@ interface TocGroupProps {
14531453
function TocGroup({ title, icon, items }: TocGroupProps) {
14541454
// Sort items alphabetically to match content order
14551455
const sortedItems = [...items].sort((a, b) => a.name.localeCompare(b.name));
1456+
// Create anchor ID from title (e.g., "Classes" -> "category-classes")
1457+
const anchorId = `category-${title.toLowerCase()}`;
14561458

14571459
return (
14581460
<div class="toc-group">
14591461
<h4>
1460-
<i class={`ti ${icon}`} /> {title}
1462+
<a href={`#${anchorId}`}>
1463+
<i class={`ti ${icon}`} /> {title}
1464+
</a>
14611465
</h4>
14621466
<ul>
14631467
{sortedItems.map((item) => (

0 commit comments

Comments
 (0)