Skip to content

Commit 118ec6c

Browse files
committed
feat: prototyping phase for doc gen
1 parent 6c27312 commit 118ec6c

16 files changed

Lines changed: 2311 additions & 2 deletions

File tree

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@jsr:registry=https://npm.jsr.io

app/pages/[...package].vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,18 @@ defineOgImageComponent('Package', {
739739
</a>
740740
</li>
741741

742+
<li v-if="displayVersion">
743+
<NuxtLink
744+
:to="{
745+
name: 'docs',
746+
params: { path: [...pkg.name.split('/'), 'v', displayVersion.version] },
747+
}"
748+
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
749+
>
750+
<span class="i-carbon-document w-4 h-4" aria-hidden="true" />
751+
docs
752+
</NuxtLink>
753+
</li>
742754
<li v-if="displayVersion">
743755
<NuxtLink
744756
:to="{

app/pages/docs/[...path].vue

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
<script setup lang="ts">
2+
import type { DocsResponse } from '#shared/types'
3+
4+
definePageMeta({
5+
name: 'docs',
6+
})
7+
8+
const route = useRoute('docs')
9+
const router = useRouter()
10+
11+
const parsedRoute = computed(() => {
12+
const segments = route.params.path || []
13+
const vIndex = segments.indexOf('v')
14+
15+
if (vIndex === -1 || vIndex >= segments.length - 1) {
16+
return {
17+
packageName: segments.join('/'),
18+
version: null as string | null,
19+
}
20+
}
21+
22+
return {
23+
packageName: segments.slice(0, vIndex).join('/'),
24+
version: segments.slice(vIndex + 1).join('/'),
25+
}
26+
})
27+
28+
const packageName = computed(() => parsedRoute.value.packageName)
29+
const requestedVersion = computed(() => parsedRoute.value.version)
30+
31+
const { data: pkg } = usePackage(packageName)
32+
33+
const latestVersion = computed(() => pkg.value?.['dist-tags']?.latest ?? null)
34+
35+
watch(
36+
[requestedVersion, latestVersion, packageName],
37+
([version, latest, name]) => {
38+
if (!version && latest && name) {
39+
router.replace(`/docs/${name}/v/${latest}`)
40+
}
41+
},
42+
{ immediate: true },
43+
)
44+
45+
const resolvedVersion = computed(() => requestedVersion.value ?? latestVersion.value)
46+
47+
const docsUrl = computed(() => {
48+
if (!packageName.value || !resolvedVersion.value) return null
49+
return `/api/registry/docs/${packageName.value}/v/${resolvedVersion.value}`
50+
})
51+
52+
const { data: docsData, status: docsStatus } = useLazyFetch<DocsResponse>(() => docsUrl.value!, {
53+
watch: [docsUrl],
54+
immediate: computed(() => !!docsUrl.value).value,
55+
default: () => ({
56+
package: packageName.value,
57+
version: resolvedVersion.value ?? '',
58+
html: '',
59+
toc: null,
60+
breadcrumbs: null,
61+
status: 'missing',
62+
message: 'Docs are not available for this version.',
63+
}),
64+
})
65+
66+
const pageTitle = computed(() => {
67+
if (!packageName.value) return 'API Docs - npmx'
68+
if (!resolvedVersion.value) return `${packageName.value} docs - npmx`
69+
return `${packageName.value}@${resolvedVersion.value} docs - npmx`
70+
})
71+
72+
useSeoMeta({
73+
title: () => pageTitle.value,
74+
})
75+
76+
const showLoading = computed(() => docsStatus.value === 'pending')
77+
const showEmptyState = computed(() => docsData.value?.status !== 'ok')
78+
</script>
79+
80+
<template>
81+
<div class="min-h-screen">
82+
<!-- Sticky header - positioned below AppHeader (57px) -->
83+
<header class="sticky top-[57px] z-10 bg-bg/95 backdrop-blur border-b border-border">
84+
<div class="px-4 sm:px-6 lg:px-8 py-4">
85+
<div class="flex items-center justify-between gap-4">
86+
<div class="flex items-center gap-3 min-w-0">
87+
<NuxtLink
88+
v-if="packageName"
89+
:to="`/${packageName}`"
90+
class="font-mono text-lg sm:text-xl font-semibold text-fg hover:text-fg-muted transition-colors truncate"
91+
>
92+
{{ packageName }}
93+
</NuxtLink>
94+
<span v-if="resolvedVersion" class="text-fg-subtle font-mono text-sm shrink-0">
95+
{{ resolvedVersion }}
96+
</span>
97+
</div>
98+
<div class="flex items-center gap-3 shrink-0">
99+
<span class="text-xs px-2 py-1 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
100+
API Docs
101+
</span>
102+
</div>
103+
</div>
104+
</div>
105+
</header>
106+
107+
<div class="flex">
108+
<!-- Sidebar TOC -->
109+
<aside
110+
v-if="docsData?.toc && !showEmptyState"
111+
class="hidden lg:block w-64 xl:w-72 shrink-0 border-r border-border"
112+
>
113+
<div class="sticky top-[114px] h-[calc(100vh-114px)] overflow-y-auto p-4">
114+
<h2 class="text-xs font-semibold text-fg-subtle uppercase tracking-wider mb-4">
115+
Contents
116+
</h2>
117+
<!-- eslint-disable vue/no-v-html -->
118+
<div class="toc-content" v-html="docsData.toc" />
119+
</div>
120+
</aside>
121+
122+
<!-- Main content -->
123+
<main class="flex-1 min-w-0">
124+
<div v-if="showLoading" class="p-6 sm:p-8 lg:p-12 space-y-4">
125+
<div class="skeleton h-8 w-64 rounded" />
126+
<div class="skeleton h-4 w-full max-w-2xl rounded" />
127+
<div class="skeleton h-4 w-5/6 max-w-2xl rounded" />
128+
<div class="skeleton h-4 w-3/4 max-w-2xl rounded" />
129+
</div>
130+
131+
<div v-else-if="showEmptyState" class="p-6 sm:p-8 lg:p-12">
132+
<div class="max-w-xl rounded-lg border border-border bg-bg-muted p-6">
133+
<h2 class="font-mono text-lg mb-2">Docs not available</h2>
134+
<p class="text-fg-subtle text-sm">
135+
{{ docsData?.message ?? 'We could not generate docs for this version.' }}
136+
</p>
137+
<div class="flex gap-4 mt-4">
138+
<NuxtLink
139+
v-if="packageName"
140+
:to="`/${packageName}`"
141+
class="link-subtle font-mono text-sm"
142+
>
143+
View package
144+
</NuxtLink>
145+
</div>
146+
</div>
147+
</div>
148+
149+
<!-- eslint-disable vue/no-v-html -->
150+
<div v-else class="docs-content p-6 sm:p-8 lg:p-12" v-html="docsData?.html" />
151+
</main>
152+
</div>
153+
</div>
154+
</template>
155+
156+
<style>
157+
/* Table of contents styles */
158+
.toc-content ul {
159+
@apply space-y-1;
160+
}
161+
162+
.toc-content > ul > li {
163+
@apply mb-4;
164+
}
165+
166+
.toc-content > ul > li > a {
167+
@apply text-sm font-medium text-fg-muted hover:text-fg;
168+
}
169+
170+
.toc-content > ul > li > ul {
171+
@apply mt-2 pl-3 border-l border-border/50;
172+
}
173+
174+
.toc-content > ul > li > ul a {
175+
@apply text-xs text-fg-subtle hover:text-fg block py-0.5 truncate;
176+
}
177+
178+
/* Main docs content container - no max-width to use full space */
179+
.docs-content {
180+
@apply max-w-none;
181+
}
182+
183+
/* Section headings */
184+
.docs-content .docs-section {
185+
@apply mb-16;
186+
}
187+
188+
.docs-content .docs-section-title {
189+
@apply text-lg font-semibold text-fg mb-8 pb-3 pt-4 border-b border-border;
190+
@apply sticky bg-bg z-[2];
191+
/* Stick below both headers: AppHeader (57px) + docs header (~57px) = 114px */
192+
top: 114px;
193+
}
194+
195+
/* Individual symbol articles */
196+
.docs-content .docs-symbol {
197+
@apply mb-10 pb-10 border-b border-border/30 last:border-0;
198+
}
199+
200+
.docs-content .docs-symbol:target {
201+
@apply scroll-mt-32;
202+
}
203+
204+
.docs-content .docs-symbol:target .docs-symbol-header {
205+
@apply bg-amber-500/10 -mx-3 px-3 py-1 rounded-md;
206+
}
207+
208+
/* Symbol header (name + badges) */
209+
.docs-content .docs-symbol-header {
210+
@apply flex items-center gap-3 mb-4 flex-wrap;
211+
}
212+
213+
.docs-content .docs-anchor {
214+
@apply text-fg-subtle/50 hover:text-fg-subtle transition-colors text-lg no-underline;
215+
}
216+
217+
.docs-content .docs-symbol-name {
218+
@apply font-mono text-lg font-semibold text-fg m-0;
219+
}
220+
221+
/* Badges */
222+
.docs-content .docs-badge {
223+
@apply text-xs px-2 py-0.5 rounded-full font-medium;
224+
}
225+
226+
.docs-content .docs-badge--function { @apply bg-blue-500/15 text-blue-400; }
227+
.docs-content .docs-badge--class { @apply bg-amber-500/15 text-amber-400; }
228+
.docs-content .docs-badge--interface { @apply bg-emerald-500/15 text-emerald-400; }
229+
.docs-content .docs-badge--typeAlias { @apply bg-violet-500/15 text-violet-400; }
230+
.docs-content .docs-badge--variable { @apply bg-orange-500/15 text-orange-400; }
231+
.docs-content .docs-badge--enum { @apply bg-pink-500/15 text-pink-400; }
232+
.docs-content .docs-badge--namespace { @apply bg-cyan-500/15 text-cyan-400; }
233+
.docs-content .docs-badge--async { @apply bg-purple-500/15 text-purple-400; }
234+
235+
/* Signature code block - now uses Shiki */
236+
.docs-content .docs-signature {
237+
@apply mb-5;
238+
}
239+
240+
.docs-content .docs-signature .shiki {
241+
@apply text-sm bg-bg-muted/50 border border-border/50 p-4 rounded-lg;
242+
white-space: pre-wrap;
243+
word-break: break-word;
244+
}
245+
246+
.docs-content .docs-signature .shiki code {
247+
@apply text-sm;
248+
white-space: pre-wrap;
249+
}
250+
251+
/* Overload count badge */
252+
.docs-content .docs-overload-count {
253+
@apply text-xs text-fg-subtle;
254+
}
255+
256+
/* More overloads indicator */
257+
.docs-content .docs-more-overloads {
258+
@apply text-xs text-fg-subtle italic mt-2 mb-0;
259+
}
260+
261+
/* Description text */
262+
.docs-content .docs-description {
263+
@apply text-sm text-fg-muted leading-relaxed mb-5;
264+
}
265+
266+
.docs-content .docs-description code {
267+
@apply bg-bg-muted px-1.5 py-0.5 rounded text-xs font-mono;
268+
}
269+
270+
/* Deprecation warning */
271+
.docs-content .docs-deprecated {
272+
@apply bg-amber-500/10 border border-amber-500/20 rounded-lg p-4 mb-5;
273+
}
274+
275+
.docs-content .docs-deprecated strong {
276+
@apply text-amber-400 text-sm;
277+
}
278+
279+
.docs-content .docs-deprecated p {
280+
@apply text-amber-300/80 text-sm mt-2 mb-0;
281+
}
282+
283+
/* Parameters, Returns, Examples, See Also sections */
284+
.docs-content .docs-params,
285+
.docs-content .docs-returns,
286+
.docs-content .docs-examples,
287+
.docs-content .docs-see,
288+
.docs-content .docs-members {
289+
@apply mb-5;
290+
}
291+
292+
.docs-content .docs-params h4,
293+
.docs-content .docs-returns h4,
294+
.docs-content .docs-examples h4,
295+
.docs-content .docs-see h4,
296+
.docs-content .docs-members h4 {
297+
@apply text-xs font-semibold text-fg-subtle uppercase tracking-wider mb-3;
298+
}
299+
300+
/* Definition lists for params/members */
301+
.docs-content dl {
302+
@apply space-y-2;
303+
}
304+
305+
.docs-content dt {
306+
@apply font-mono text-sm text-fg-muted;
307+
}
308+
309+
.docs-content dd {
310+
@apply text-sm text-fg-subtle ml-4 mb-3;
311+
}
312+
313+
/* Returns paragraph */
314+
.docs-content .docs-returns p {
315+
@apply text-sm text-fg-muted m-0;
316+
}
317+
318+
/* Example code blocks - now uses Shiki */
319+
.docs-content .docs-examples .shiki {
320+
@apply text-sm bg-bg-muted border border-border/50 p-4 rounded-lg overflow-x-auto mb-3;
321+
}
322+
323+
.docs-content .docs-examples .shiki code {
324+
@apply text-sm;
325+
}
326+
327+
/* See also list */
328+
.docs-content .docs-see ul {
329+
@apply list-disc list-inside text-sm text-fg-muted space-y-1;
330+
}
331+
332+
.docs-content .docs-link {
333+
@apply text-blue-400 hover:text-blue-300 underline underline-offset-2;
334+
}
335+
336+
/* Symbol cross-reference links */
337+
.docs-content .docs-symbol-link {
338+
@apply text-emerald-400 hover:text-emerald-300 underline underline-offset-2;
339+
}
340+
341+
/* Unknown symbol references shown as code */
342+
.docs-content .docs-symbol-ref {
343+
@apply bg-bg-muted px-1.5 py-0.5 rounded text-xs font-mono;
344+
}
345+
346+
/* Inline code in descriptions */
347+
.docs-content .docs-inline-code {
348+
@apply bg-bg-muted px-1.5 py-0.5 rounded text-xs font-mono;
349+
}
350+
351+
/* Enum members */
352+
.docs-content .docs-enum-members {
353+
@apply flex flex-wrap gap-2 list-none p-0;
354+
}
355+
356+
.docs-content .docs-enum-members li {
357+
@apply m-0;
358+
}
359+
360+
.docs-content .docs-enum-members code {
361+
@apply text-sm font-mono text-fg-muted bg-bg-muted px-2 py-1 rounded;
362+
}
363+
364+
/* Members section (constructors, properties, methods) */
365+
.docs-content .docs-members pre {
366+
@apply text-sm bg-bg-muted/50 border border-border/50 p-3 rounded-lg overflow-x-auto font-mono;
367+
}
368+
369+
.docs-content .docs-members pre code {
370+
@apply text-fg-muted;
371+
}
372+
</style>

0 commit comments

Comments
 (0)