Skip to content

Commit 9d1a219

Browse files
committed
Improve docs layout and right-side On this page TOC
1 parent 37a4a45 commit 9d1a219

16 files changed

Lines changed: 1902 additions & 114 deletions

src/components/DocsLayout.tsx

Lines changed: 135 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1-
import React from 'react';
2-
import { NavLink } from 'react-router-dom';
1+
import React, { useState } from 'react';
2+
import { NavLink, useLocation } from 'react-router-dom';
33

4-
const sections = [
4+
interface SidebarSection {
5+
title: string;
6+
icon: React.ReactNode;
7+
items: { label: string; to: string }[];
8+
}
9+
10+
const sections: SidebarSection[] = [
511
{
612
title: 'Getting Started',
13+
icon: (
14+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.63 8.41m5.96 5.96a14.926 14.926 0 01-5.84 2.58m0 0a14.926 14.926 0 01-5.84-2.58" />
16+
</svg>
17+
),
718
items: [
819
{ label: 'Linux', to: '/docs/installation/linux' },
920
{ label: 'macOS', to: '/docs/installation/macos' },
@@ -12,53 +23,150 @@ const sections = [
1223
},
1324
{
1425
title: 'Usage',
26+
icon: (
27+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
28+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
29+
</svg>
30+
),
1531
items: [
1632
{ label: 'CLI Commands', to: '/docs/usage' },
33+
{ label: 'Configuration', to: '/docs/configuration' },
34+
],
35+
},
36+
{
37+
title: 'Images',
38+
icon: (
39+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
40+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 7.5l-2.25-1.313M21 7.5v2.25m0-2.25l-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2.25 1.313M3 7.5v2.25m9 3l2.25-1.313M12 12.75l-2.25-1.313M12 12.75V15m0 6.75l2.25-1.313M12 21.75V19.5m0 2.25l-2.25-1.313m0-16.875L12 2.25l2.25 1.313M21 14.25v2.25l-2.25 1.313m-13.5 0L3 16.5v-2.25" />
41+
</svg>
42+
),
43+
items: [
44+
{ label: 'Available Images', to: '/docs/images' },
45+
],
46+
},
47+
{
48+
title: 'nihil-history',
49+
icon: (
50+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
51+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
52+
</svg>
53+
),
54+
items: [
55+
{ label: 'Overview & CLI', to: '/docs/nihil-history' },
1756
],
1857
},
1958
{
2059
title: 'Features',
60+
icon: (
61+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
62+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
63+
</svg>
64+
),
2165
items: [
2266
{ label: 'Shell Completion', to: '/docs/completion' },
2367
{ label: 'Command History', to: '/docs/history' },
2468
],
2569
},
70+
{
71+
title: 'Project',
72+
icon: (
73+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
74+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
75+
</svg>
76+
),
77+
items: [
78+
{ label: 'Architecture', to: '/docs/architecture' },
79+
{ label: 'Contributing', to: '/docs/contributing' },
80+
],
81+
},
2682
{
2783
title: 'More',
84+
icon: (
85+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
86+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
87+
</svg>
88+
),
2889
items: [
2990
{ label: 'FAQ', to: '/docs/faq' },
3091
{ label: 'About', to: '/docs/about' },
3192
],
3293
},
3394
];
3495

96+
const SidebarSection: React.FC<{ section: SidebarSection; defaultOpen: boolean }> = ({ section, defaultOpen }) => {
97+
const [open, setOpen] = useState(defaultOpen);
98+
99+
return (
100+
<div>
101+
<button
102+
onClick={() => setOpen(!open)}
103+
className="flex items-center gap-2 w-full px-3 py-2 rounded-lg text-left group transition-colors hover:bg-slate-800/40"
104+
>
105+
<span className="text-slate-500 group-hover:text-amber-400 transition-colors">
106+
{section.icon}
107+
</span>
108+
<span className="text-[12px] font-semibold uppercase tracking-wider text-slate-400 group-hover:text-slate-200 transition-colors flex-1">
109+
{section.title}
110+
</span>
111+
<svg
112+
className={
113+
'w-3.5 h-3.5 text-slate-600 transition-transform duration-200 ' +
114+
(open ? 'rotate-90' : '')
115+
}
116+
fill="none"
117+
stroke="currentColor"
118+
viewBox="0 0 24 24"
119+
>
120+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
121+
</svg>
122+
</button>
123+
<div
124+
className={
125+
'overflow-hidden transition-all duration-200 ' +
126+
(open ? 'max-h-96 opacity-100 mt-1' : 'max-h-0 opacity-0')
127+
}
128+
>
129+
<nav className="space-y-0.5 ml-3 pl-3 border-l border-slate-800/80">
130+
{section.items.map((item) => (
131+
<NavLink
132+
key={item.to}
133+
to={item.to}
134+
className={({ isActive }) =>
135+
'block px-3 py-1.5 rounded-md text-[13px] transition-all duration-150 ' +
136+
(isActive
137+
? 'bg-gradient-to-r from-amber-400/10 to-transparent text-amber-300 font-medium border-l-2 border-amber-400 -ml-[13px] pl-[23px]'
138+
: 'text-slate-500 hover:text-slate-200 hover:bg-slate-800/40')
139+
}
140+
>
141+
{item.label}
142+
</NavLink>
143+
))}
144+
</nav>
145+
</div>
146+
</div>
147+
);
148+
};
149+
35150
export const DocsLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
151+
const location = useLocation();
152+
153+
// Auto-open the section that contains the current page
154+
const isInSection = (section: SidebarSection) =>
155+
section.items.some((item) => location.pathname === item.to);
156+
36157
return (
37-
<div className="flex gap-10 items-start">
38-
<aside className="w-48 shrink-0 sticky top-24 hidden md:block">
39-
<div className="space-y-6">
158+
<div className="flex gap-12 items-start">
159+
<aside className="w-60 shrink-0 sticky top-24 hidden md:block">
160+
<div className="space-y-1 p-2 rounded-xl bg-slate-900/30 border border-slate-800/50 backdrop-blur-sm">
161+
<div className="px-3 pt-1 pb-2 mb-1 border-b border-slate-800/50">
162+
<p className="text-[10px] font-bold uppercase tracking-[0.25em] text-slate-600">Documentation</p>
163+
</div>
40164
{sections.map((section) => (
41-
<div key={section.title}>
42-
<p className="text-[11px] font-semibold uppercase tracking-widest text-slate-500 mb-2 px-3">
43-
{section.title}
44-
</p>
45-
<nav className="space-y-0.5">
46-
{section.items.map((item) => (
47-
<NavLink
48-
key={item.to}
49-
to={item.to}
50-
className={({ isActive }) =>
51-
'block px-3 py-1.5 rounded-md text-sm transition-colors ' +
52-
(isActive
53-
? 'bg-amber-400/10 text-amber-300 font-medium'
54-
: 'text-slate-400 hover:text-white hover:bg-slate-800/60')
55-
}
56-
>
57-
{item.label}
58-
</NavLink>
59-
))}
60-
</nav>
61-
</div>
165+
<SidebarSection
166+
key={section.title}
167+
section={section}
168+
defaultOpen={isInSection(section)}
169+
/>
62170
))}
63171
</div>
64172
</aside>

src/components/Layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
3333
<div className="min-h-screen bg-black text-slate-100 flex flex-col relative">
3434
<FlyingPigeons />
3535
<header className="sticky top-0 z-50 border-b border-slate-800/80 bg-black/85 backdrop-blur-xl">
36-
<div className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between gap-4">
36+
<div className="max-w-[96rem] mx-auto px-4 py-3 flex items-center justify-between gap-4">
3737

3838
{/* Logo */}
3939
<Link to="/" className="flex items-center gap-3 group">
@@ -111,11 +111,11 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
111111
</header>
112112

113113
<main className="flex-1">
114-
<div className="max-w-6xl mx-auto px-4 py-10">{children}</div>
114+
<div className="max-w-[96rem] mx-auto px-4 py-10">{children}</div>
115115
</main>
116116

117117
<footer className="border-t border-slate-800 bg-black/90 text-xs text-slate-500">
118-
<div className="max-w-6xl mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-2">
118+
<div className="max-w-[96rem] mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-2">
119119
<span>
120120
Built by{' '}
121121
<a href="https://github.com/0xbbuddha" target="_blank" rel="noreferrer"

src/components/SectionToc.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React, { useEffect, useMemo, useState } from 'react';
2+
3+
type TocItem = {
4+
id: string;
5+
label: string;
6+
};
7+
8+
export const SectionToc: React.FC<{ items: TocItem[] }> = ({ items }) => {
9+
const [activeId, setActiveId] = useState<string>(items[0]?.id ?? '');
10+
const [progress, setProgress] = useState(0);
11+
12+
const ids = useMemo(() => items.map((item) => item.id), [items]);
13+
14+
useEffect(() => {
15+
if (!ids.length) return;
16+
17+
const markerOffset = Math.floor(window.innerHeight * 0.32);
18+
let ticking = false;
19+
20+
const update = () => {
21+
ticking = false;
22+
const sections = ids
23+
.map((id) => document.getElementById(id))
24+
.filter((el): el is HTMLElement => Boolean(el));
25+
26+
if (!sections.length) return;
27+
28+
const markerY = window.scrollY + markerOffset;
29+
let current = sections[0].id;
30+
31+
for (const section of sections) {
32+
if (section.offsetTop <= markerY) {
33+
current = section.id;
34+
} else {
35+
break;
36+
}
37+
}
38+
setActiveId(current);
39+
40+
const firstTop = sections[0].offsetTop;
41+
const last = sections[sections.length - 1];
42+
const lastBottom = last.offsetTop + last.offsetHeight;
43+
const total = Math.max(lastBottom - firstTop, 1);
44+
const pct = Math.min(Math.max((markerY - firstTop) / total, 0), 1);
45+
setProgress(pct);
46+
};
47+
48+
const onScrollOrResize = () => {
49+
if (!ticking) {
50+
ticking = true;
51+
window.requestAnimationFrame(update);
52+
}
53+
};
54+
55+
update();
56+
window.addEventListener('scroll', onScrollOrResize, { passive: true });
57+
window.addEventListener('resize', onScrollOrResize);
58+
59+
return () => {
60+
window.removeEventListener('scroll', onScrollOrResize);
61+
window.removeEventListener('resize', onScrollOrResize);
62+
};
63+
}, [ids]);
64+
65+
return (
66+
<aside className="sticky top-24 hidden sm:block w-[220px] lg:w-[260px]">
67+
<div className="rounded-2xl border border-slate-800/70 bg-slate-900/45 p-4 backdrop-blur-sm">
68+
<p className="text-xs uppercase tracking-widest text-slate-500 font-semibold">
69+
On this page
70+
</p>
71+
<nav className="relative mt-4 pl-5 space-y-2">
72+
<span className="absolute left-0 top-0 h-full w-[2px] bg-slate-800/80" />
73+
<span
74+
className="absolute left-0 top-0 w-[2px] bg-gradient-to-b from-amber-300 to-amber-500 transition-all duration-200"
75+
style={{ height: `${Math.max(progress * 100, 5)}%` }}
76+
/>
77+
{items.map((item) => {
78+
const isActive = item.id === activeId;
79+
return (
80+
<a
81+
key={item.id}
82+
href={`#${item.id}`}
83+
className={
84+
'block text-sm leading-5 transition-colors ' +
85+
(isActive ? 'text-amber-300 font-medium' : 'text-slate-400 hover:text-amber-300')
86+
}
87+
>
88+
{item.label}
89+
</a>
90+
);
91+
})}
92+
</nav>
93+
</div>
94+
</aside>
95+
);
96+
};

src/pages/docs/AboutPage.tsx

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import React from 'react';
22
import { Link } from 'react-router-dom';
3+
import { SectionToc } from '../../components/SectionToc';
34

45
export const AboutPage: React.FC = () => {
56
return (
6-
<div className="space-y-8 max-w-4xl">
7+
<div className="space-y-8 w-full">
78
<header className="space-y-3">
89
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">
910
Docs / <span className="text-amber-400">About</span>
@@ -17,7 +18,7 @@ export const AboutPage: React.FC = () => {
1718
</p>
1819
</header>
1920

20-
<div className="grid md:grid-cols-[1fr,200px] gap-8 items-start">
21+
<div className="grid sm:grid-cols-[minmax(0,_1fr)_180px] gap-8 items-start">
2122
<div className="space-y-10 min-w-0">
2223

2324
{/* Why */}
@@ -134,9 +135,9 @@ export const AboutPage: React.FC = () => {
134135
<h2 className="text-xl font-semibold text-white">Useful links</h2>
135136
<div className="space-y-2 text-sm">
136137
{[
137-
{ label: 'GitHub nihil wrapper', url: 'https://github.com/TheNullPigeons/nihil' },
138-
{ label: 'GitHub nihil images', url: 'https://github.com/TheNullPigeons/nihil-images' },
139-
{ label: 'GitHub TheNullPigeons org', url: 'https://github.com/TheNullPigeons' },
138+
{ label: 'GitHub - nihil wrapper', url: 'https://github.com/TheNullPigeons/nihil' },
139+
{ label: 'GitHub - nihil images', url: 'https://github.com/TheNullPigeons/nihil-images' },
140+
{ label: 'GitHub - TheNullPigeons org', url: 'https://github.com/TheNullPigeons' },
140141
{ label: 'Report a bug', url: 'https://github.com/TheNullPigeons/nihil/issues' },
141142
].map(({ label, url }) => (
142143
<a
@@ -157,17 +158,15 @@ export const AboutPage: React.FC = () => {
157158

158159
</div>
159160

160-
{/* On this page */}
161-
<aside className="space-y-3 text-sm sticky top-24 hidden md:block">
162-
<p className="text-slate-500 font-medium">On this page</p>
163-
<nav className="space-y-1">
164-
<a href="#why" className="block text-slate-400 hover:text-amber-300 text-xs">Why we built this</a>
165-
<a href="#components" className="block text-slate-400 hover:text-amber-300 text-xs">What Nihil is</a>
166-
<a href="#design" className="block text-slate-400 hover:text-amber-300 text-xs">Design choices</a>
167-
<a href="#team" className="block text-slate-400 hover:text-amber-300 text-xs">Who we are</a>
168-
<a href="#links" className="block text-slate-400 hover:text-amber-300 text-xs">Useful links</a>
169-
</nav>
170-
</aside>
161+
<SectionToc
162+
items={[
163+
{ id: 'why', label: 'Why we built this' },
164+
{ id: 'components', label: 'What Nihil is' },
165+
{ id: 'design', label: 'Design choices' },
166+
{ id: 'team', label: 'Who we are' },
167+
{ id: 'links', label: 'Useful links' },
168+
]}
169+
/>
171170
</div>
172171
</div>
173172
);

0 commit comments

Comments
 (0)