Skip to content

Commit 56bc856

Browse files
committed
feat: better looking docs
1 parent 302e468 commit 56bc856

9 files changed

Lines changed: 169 additions & 55 deletions

File tree

src/components/docs/MDXRenderer.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ function buildMdxComponents(projectId?: string, version?: string) {
120120
if (!className) {
121121
return (
122122
<code
123-
className="bg-white/10 text-white px-1.5 py-0.5 rounded font-mono text-[0.9em]"
123+
className="code-inline"
124124
{...props}
125125
>
126126
{children}
@@ -264,7 +264,7 @@ export const MDXRenderer = memo(({ content, className, projectId, version }: MDX
264264

265265
if (error) {
266266
return (
267-
<div className={cn('prose prose-invert max-w-none', className)}>
267+
<div className={cn('prose prose-invert', className)}>
268268
<div className="p-4 border border-red-500/50 bg-red-500/10 rounded-lg">
269269
<h3 className="text-red-400 mb-2">MDX Compilation Error</h3>
270270
<pre className="text-sm text-red-300 overflow-auto">
@@ -277,16 +277,19 @@ export const MDXRenderer = memo(({ content, className, projectId, version }: MDX
277277

278278
if (!MDXContent) {
279279
return (
280-
<div className={cn('prose prose-invert max-w-none', className)}>
281-
<div className="flex items-center justify-center py-8">
282-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
280+
<div className={cn('prose prose-invert', className)}>
281+
<div className="space-y-3 py-8">
282+
<div className="h-4 bg-muted rounded w-full animate-pulse" />
283+
<div className="h-4 bg-muted rounded w-11/12 animate-pulse" />
284+
<div className="h-4 bg-muted rounded w-4/5 animate-pulse" />
285+
<div className="h-4 bg-muted rounded w-3/4 animate-pulse" />
283286
</div>
284287
</div>
285288
);
286289
}
287290

288291
return (
289-
<div className={cn('prose prose-invert max-w-none', className)}>
292+
<div className={cn('prose prose-invert', className)}>
290293
<MDXContent components={mdxComponents} />
291294
</div>
292295
);

src/components/docs/MarkdownRenderer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function buildComponents(projectId?: string, version?: string) {
101101
if (inline || !className) {
102102
return (
103103
<code
104-
className="bg-white/10 text-white px-1.5 py-0.5 rounded font-mono text-[0.9em]"
104+
className="code-inline"
105105
{...props}
106106
>
107107
{children}
@@ -209,7 +209,7 @@ export const MarkdownRenderer = memo(({ content, className, projectId, version }
209209
const components = useMemo(() => buildComponents(projectId, version), [projectId, version]);
210210

211211
return (
212-
<div className={cn('prose prose-invert max-w-none', className)}>
212+
<div className={cn('prose prose-invert', className)}>
213213
<ReactMarkdown
214214
remarkPlugins={remarkPlugins}
215215
rehypePlugins={rehypePlugins}

src/components/docs/SmartImg.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* - Defaults to `loading="lazy"` + `decoding="async"`.
1212
*/
1313

14-
import { useEffect, useState } from 'react';
14+
import { useEffect, useState, useCallback } from 'react';
1515

1616
interface ImageMeta {
1717
width?: number;
@@ -73,16 +73,13 @@ export function SmartImg({
7373
...rest
7474
}: Props) {
7575
const resolved = src ? normalizeAssetPath(src) : src;
76+
const [erroredImages, setErroredImages] = useState<Set<string>>(new Set());
7677
const [meta, setMeta] = useState<ImageMeta | undefined>(() =>
7778
resolved && manifestCache ? manifestCache[resolved] : undefined
7879
);
7980

8081
useEffect(() => {
8182
if (!resolved) return;
82-
if (manifestCache) {
83-
setMeta(manifestCache[resolved]);
84-
return;
85-
}
8683
let alive = true;
8784
loadManifest().then(m => {
8885
if (alive) setMeta(m[resolved]);
@@ -92,18 +89,42 @@ export function SmartImg({
9289
};
9390
}, [resolved]);
9491

92+
const handleError = useCallback(() => {
93+
setErroredImages(prev => {
94+
if (!resolved) return prev;
95+
const next = new Set(prev);
96+
next.add(resolved);
97+
return next;
98+
});
99+
}, [resolved]);
100+
95101
// Explicit width/height props always win; fall back to manifest.
96102
const finalWidth = width ?? meta?.width;
97103
const finalHeight = height ?? meta?.height;
98104

105+
if (!resolved || (resolved && erroredImages.has(resolved))) {
106+
return (
107+
<span
108+
className={`inline-flex items-center gap-1.5 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground ${className ?? ''}`}
109+
role="img"
110+
aria-label={alt ?? 'Image failed to load'}
111+
>
112+
<span aria-hidden="true">&#x1F5BC;</span>
113+
{alt || 'Image'}
114+
</span>
115+
);
116+
}
117+
99118
return (
100119
<img
120+
key={resolved}
101121
src={resolved}
102122
alt={alt ?? ''}
103123
width={finalWidth}
104124
height={finalHeight}
105125
loading={loading}
106126
decoding={decoding}
127+
onError={handleError}
107128
className={className}
108129
{...rest}
109130
/>

src/components/docs/TableOfContents.tsx

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ export function TableOfContents({ items, className }: TableOfContentsProps) {
1212
// 1. Intersection Observer for Scroll Tracking
1313
const [activeIds, setActiveIds] = useState<string[]>([]);
1414
const [highlighterStyle, setHighlighterStyle] = useState({ top: 0, height: 0, opacity: 0 });
15-
const [tocOffset, setTocOffset] = useState(0);
1615
const lastActiveIdRef = useRef<string | null>(null);
1716
const intersectingIdsRef = useRef(new Set<string>());
1817
const itemsRef = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
1918
const navRef = useRef<HTMLElement | null>(null);
20-
const contentRef = useRef<HTMLDivElement | null>(null);
2119

2220
useEffect(() => {
2321
const observerOptions = {
@@ -159,20 +157,6 @@ export function TableOfContents({ items, className }: TableOfContentsProps) {
159157
setHighlighterStyle(s => ({ ...s, opacity: 0 }));
160158
}, [activeIds, items]);
161159

162-
useEffect(() => {
163-
const activeId = activeIds[activeIds.length - 1];
164-
const activeEl = activeId ? itemsRef.current[activeId] : null;
165-
const navEl = navRef.current;
166-
const contentEl = contentRef.current;
167-
if (!activeEl || !navEl || !contentEl) return;
168-
169-
const activeAnchor = Math.max(36, navEl.clientHeight * 0.28);
170-
const activeTop = activeEl.offsetTop;
171-
const maxOffset = Math.max(0, contentEl.scrollHeight - navEl.clientHeight);
172-
173-
setTocOffset(Math.min(Math.max(activeTop - activeAnchor, 0), maxOffset));
174-
}, [activeIds]);
175-
176160
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
177161
e.preventDefault();
178162
const element = document.getElementById(id);
@@ -205,11 +189,7 @@ export function TableOfContents({ items, className }: TableOfContentsProps) {
205189
className="relative max-h-[calc(100vh-10rem)] overflow-hidden pr-2"
206190
aria-labelledby="toc-heading"
207191
>
208-
<div
209-
ref={contentRef}
210-
className="relative transition-transform duration-300 ease-in-out"
211-
style={{ transform: `translateY(-${tocOffset}px)` }}
212-
>
192+
<div className="relative">
213193
<div
214194
className="absolute left-0 top-0 bottom-0 w-6 bg-border/40 pointer-events-none transition-all duration-300"
215195
style={{

src/components/layout/Sidebar.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,31 @@ interface CategorySectionProps {
8686
versionId: string;
8787
}
8888

89+
const CATEGORY_COLORS = [
90+
'bg-sky-500',
91+
'bg-emerald-500',
92+
'bg-violet-500',
93+
'bg-amber-500',
94+
'bg-rose-500',
95+
'bg-cyan-500',
96+
'bg-lime-500',
97+
'bg-fuchsia-500',
98+
'bg-orange-500',
99+
'bg-teal-500',
100+
];
101+
102+
function categoryColorIndex(name: string): number {
103+
let hash = 0;
104+
for (let i = 0; i < name.length; i++) {
105+
hash = ((hash << 5) - hash) + name.charCodeAt(i);
106+
}
107+
return Math.abs(hash) % CATEGORY_COLORS.length;
108+
}
109+
89110
function CategorySection({ category, currentSlug, projectId, versionId }: CategorySectionProps) {
90111
const [expanded, setExpanded] = useState(true);
91112
const regionId = `cat-${projectId}-${versionId}-${category.name.replace(/\s+/g, '-').toLowerCase()}`;
113+
const dotColor = CATEGORY_COLORS[categoryColorIndex(category.name)];
92114

93115
return (
94116
<div className="mb-4">
@@ -98,6 +120,7 @@ function CategorySection({ category, currentSlug, projectId, versionId }: Catego
98120
aria-controls={regionId}
99121
className="flex items-center gap-1 w-full px-3 py-2 text-sm font-semibold text-muted-foreground hover:text-foreground transition-colors"
100122
>
123+
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dotColor}`} aria-hidden="true" />
101124
{expanded ? (
102125
<ChevronDown className="h-4 w-4" aria-hidden="true" />
103126
) : (
@@ -228,7 +251,8 @@ export function MobileSidebar({ project, version }: { project: DocProject; versi
228251
<nav aria-label="Documentation sections">
229252
{version.categories.map((category) => (
230253
<div key={category.name} className="mb-4">
231-
<p className="px-3 py-2 text-sm font-semibold text-muted-foreground">
254+
<p className="flex items-center gap-1 px-3 py-2 text-sm font-semibold text-muted-foreground">
255+
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${CATEGORY_COLORS[categoryColorIndex(category.name)]}`} aria-hidden="true" />
232256
{category.name}
233257
</p>
234258
<ul className="ml-4 space-y-1">

src/index.css

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,14 @@
6868
--popover: 0 0% 100%;
6969
--popover-foreground: 222 47% 11%;
7070

71-
--primary: 190 95% 36%;
71+
--primary: 190 95% 28%;
7272
--primary-foreground: 0 0% 100%;
7373

7474
--secondary: 221 83% 53%;
7575
--secondary-foreground: 0 0% 100%;
7676

7777
--muted: 214 32% 94%;
78-
--muted-foreground: 215 16% 40%;
78+
--muted-foreground: 215 16% 35%;
7979

8080
--accent: 210 40% 94%;
8181
--accent-foreground: 222 47% 11%;
@@ -218,16 +218,25 @@
218218
}
219219

220220
@keyframes fadeIn {
221-
from {
222-
opacity: 0;
223-
transform: translateY(10px);
221+
from {
222+
opacity: 0;
223+
transform: translateY(6px);
224+
}
225+
226+
to {
227+
opacity: 1;
228+
transform: translateY(0);
229+
}
224230
}
225231

226-
to {
227-
opacity: 1;
228-
transform: translateY(0);
232+
@media (prefers-reduced-motion: reduce) {
233+
.fade-in {
234+
animation: none;
235+
}
236+
.animate-fadein {
237+
animation: none;
238+
}
229239
}
230-
}
231240

232241
.animate-lightning {
233242
stroke-dasharray: 400;
@@ -315,6 +324,20 @@
315324
text-wrap: balance;
316325
}
317326

327+
.code-inline {
328+
background: rgba(255, 255, 255, 0.1);
329+
color: hsl(var(--foreground));
330+
padding: 0.15em 0.4em;
331+
border-radius: 0.375rem;
332+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
333+
font-size: 0.9em;
334+
}
335+
336+
.light .code-inline {
337+
background: rgba(0, 0, 0, 0.08);
338+
color: hsl(var(--foreground));
339+
}
340+
318341
/* Hide scrollbar but keep functionality */
319342
.scrollbar-hide {
320343
-ms-overflow-style: none;

src/pages/Docs.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,22 @@ export function Docs() {
5757
<div className="min-h-screen flex flex-col bg-docs">
5858
<Header />
5959
<div className="flex-1 container mx-auto px-4 flex items-center justify-center">
60-
<div className="text-center">
61-
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
62-
<p className="text-muted-foreground">Loading documentation...</p>
63-
</div>
60+
<div className="w-full max-w-4xl mx-auto py-12">
61+
<div className="flex gap-8 mb-10">
62+
<div className="hidden lg:block w-64 shrink-0 space-y-3">
63+
{[75, 55, 65, 45, 60].map((w, i) => (
64+
<div key={i} className="h-4 bg-muted rounded animate-pulse" style={{ width: `${w}%` }} />
65+
))}
66+
</div>
67+
<div className="flex-1 space-y-4">
68+
<div className="h-6 bg-muted rounded w-48 animate-pulse mb-2" />
69+
<div className="h-10 bg-muted rounded w-3/4 animate-pulse" />
70+
<div className="h-4 bg-muted rounded w-full animate-pulse" />
71+
<div className="h-4 bg-muted rounded w-5/6 animate-pulse" />
72+
<div className="h-4 bg-muted rounded w-4/6 animate-pulse" />
73+
</div>
74+
</div>
75+
</div>
6476
</div>
6577
<Footer />
6678
</div>
@@ -178,17 +190,23 @@ export function Docs() {
178190
</div>
179191

180192
{isLoading ? (
181-
<div className="py-20 text-center">
182-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
183-
<p className="text-muted-foreground">Loading page...</p>
193+
<div className="py-12 space-y-4">
194+
<div className="h-4 bg-muted rounded w-40 animate-pulse mb-6" />
195+
<div className="h-10 bg-muted rounded w-3/5 animate-pulse mb-4" />
196+
<div className="h-4 bg-muted rounded w-full animate-pulse" />
197+
<div className="h-4 bg-muted rounded w-11/12 animate-pulse" />
198+
<div className="h-4 bg-muted rounded w-4/5 animate-pulse" />
199+
<div className="h-4 bg-muted rounded w-full animate-pulse" />
200+
<div className="h-4 bg-muted rounded w-3/4 animate-pulse" />
201+
<div className="h-4 bg-muted rounded w-5/6 animate-pulse" />
184202
</div>
185203
) : !doc ? (
186204
<div className="py-20 text-center">
187205
<p className="text-4xl font-bold mb-4">404</p>
188206
<p className="text-muted-foreground">This page doesn&apos;t exist.</p>
189207
</div>
190208
) : (
191-
<div key={slug} className="animate-fadein">
209+
<div key={slug} className="animate-fadein mx-auto max-w-4xl">
192210
{/* Document Header */}
193211
<div className="mb-8">
194212
{breadcrumbs && <Breadcrumbs items={breadcrumbs} className="mb-4" />}

0 commit comments

Comments
 (0)