Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
92ddf1e
feat: prototyping phase for doc gen
devdumpling Jan 25, 2026
d87bfcd
cleanup old prototype
devdumpling Jan 25, 2026
3d1221d
fix: polish and cleanup post prototype
devdumpling Jan 25, 2026
5830860
version selector
devdumpling Jan 25, 2026
582fd36
test: add basic e2e and unit tests
devdumpling Jan 25, 2026
6aaf681
fix: some validation issues with markdown rendering and parsing
devdumpling Jan 26, 2026
24017c0
chore: some planning, remove old typedoc ref
devdumpling Jan 26, 2026
6d7a091
fix: type issue with DocsResponse default status
devdumpling Jan 26, 2026
c714835
fix: align code link and prefer package docs URL
devdumpling Jan 26, 2026
791dc8f
feat-docs-wasm-attempt
devdumpling Jan 26, 2026
ff78f69
fix: use rollupConfig and externals for deno doc module resolution
devdumpling Jan 27, 2026
759dbfc
fix: fence code block regex handles ansi, edge cases, shiki highlight…
devdumpling Jan 27, 2026
6e3cdfc
[autofix.ci] apply automated fixes
autofix-ci[bot] Jan 27, 2026
b78ddf0
fix: add translation keys for docs and socket nav links
devdumpling Jan 27, 2026
e277a2d
fix: update manifest with upstream
devdumpling Jan 27, 2026
ba0ec18
fix: docs link and homepage coexist
devdumpling Jan 27, 2026
c06b1b2
chore: move docs api version back to v1 for caching
devdumpling Jan 27, 2026
0299f87
fix: handle invalid urls and errors generating docs
danielroe Jan 27, 2026
58dbc53
fix: some a11y issues
danielroe Jan 27, 2026
11bcd36
Merge origin/main into dwells-feat-docs-wasm-attempt
danielroe Jan 27, 2026
48887f1
Merge remote-tracking branch 'origin/main' into dwells-feat-docs-wasm…
danielroe Jan 27, 2026
ed424e1
Merge remote-tracking branch 'origin/main' into dwells-feat-docs-wasm…
danielroe Jan 27, 2026
d7aacaa
fix: remove socket link that was incorrectly re-added during merge
danielroe Jan 27, 2026
458ba1a
refactor: extract highlightCodeSync for shared code highlighting
danielroe Jan 27, 2026
722b3bd
chore: remove reexport
danielroe Jan 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io
5 changes: 4 additions & 1 deletion app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ const { isConnected, npmUser } = useConnector()
</script>

<template>
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
<header
aria-label="Site header"
class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border"
>
<nav aria-label="Main navigation" class="container h-14 flex items-center">
<!-- Left: Logo -->
<div class="flex-shrink-0">
Expand Down
221 changes: 221 additions & 0 deletions app/components/DocsVersionSelector.vue
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handles the docs specific version toggle.

Might want to align these components eventually (since there's another one we use on the package code page, but this one is specific to the docs page.

Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import { compare } from 'semver'

const props = defineProps<{
packageName: string
currentVersion: string
versions: Record<string, unknown>
distTags: Record<string, string>
}>()

const isOpen = ref(false)
const dropdownRef = useTemplateRef('dropdownRef')
const listboxRef = useTemplateRef('listboxRef')
const focusedIndex = ref(-1)

onClickOutside(dropdownRef, () => {
isOpen.value = false
})

/** Maximum number of versions to show in dropdown */
const MAX_VERSIONS = 10

/** Safe version comparison that falls back to string comparison on error */
function safeCompareVersions(a: string, b: string): number {
try {
return compare(a, b)
} catch {
return a.localeCompare(b)
}
}

/** Get sorted list of recent versions with their tags */
const recentVersions = computed(() => {
const versionList = Object.keys(props.versions)
.sort((a, b) => safeCompareVersions(b, a))
.slice(0, MAX_VERSIONS)

// Create a map of version -> tags
const versionTags = new Map<string, string[]>()
for (const [tag, version] of Object.entries(props.distTags)) {
const existing = versionTags.get(version)
if (existing) {
existing.push(tag)
} else {
versionTags.set(version, [tag])
}
}

return versionList.map(version => ({
version,
tags: versionTags.get(version) ?? [],
isCurrent: version === props.currentVersion,
}))
})

const latestVersion = computed(() => props.distTags.latest)

function getDocsUrl(version: string): string {
return `/docs/${props.packageName}/v/${version}`
}

function handleButtonKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
isOpen.value = false
} else if (event.key === 'ArrowDown' && !isOpen.value) {
event.preventDefault()
isOpen.value = true
focusedIndex.value = 0
}
}

function handleListboxKeydown(event: KeyboardEvent) {
const items = recentVersions.value

switch (event.key) {
case 'Escape':
isOpen.value = false
break
case 'ArrowDown':
event.preventDefault()
focusedIndex.value = Math.min(focusedIndex.value + 1, items.length - 1)
scrollToFocused()
break
case 'ArrowUp':
event.preventDefault()
focusedIndex.value = Math.max(focusedIndex.value - 1, 0)
scrollToFocused()
break
case 'Home':
event.preventDefault()
focusedIndex.value = 0
scrollToFocused()
break
case 'End':
event.preventDefault()
focusedIndex.value = items.length - 1
scrollToFocused()
break
case 'Enter':
case ' ':
event.preventDefault()
if (focusedIndex.value >= 0 && focusedIndex.value < items.length) {
navigateToVersion(items[focusedIndex.value]!.version)
}
break
}
}

function scrollToFocused() {
nextTick(() => {
const focused = listboxRef.value?.querySelector('[data-focused="true"]')
focused?.scrollIntoView({ block: 'nearest' })
})
}

function navigateToVersion(version: string) {
isOpen.value = false
navigateTo(getDocsUrl(version))
}

// Reset focused index when dropdown opens
watch(isOpen, open => {
if (open) {
const currentIdx = recentVersions.value.findIndex(v => v.isCurrent)
focusedIndex.value = currentIdx >= 0 ? currentIdx : 0
}
})
</script>

<template>
<div ref="dropdownRef" class="relative">
<button
type="button"
aria-haspopup="listbox"
:aria-expanded="isOpen"
class="flex items-center gap-1.5 text-fg-subtle font-mono text-sm hover:text-fg transition-[color] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg rounded"
@click="isOpen = !isOpen"
@keydown="handleButtonKeydown"
>
<span>{{ currentVersion }}</span>
<span
v-if="currentVersion === latestVersion"
class="text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-400 font-sans font-medium"
>
latest
</span>
<span
class="i-carbon-chevron-down w-3.5 h-3.5 transition-[transform] duration-200 motion-reduce:transition-none"
:class="{ 'rotate-180': isOpen }"
aria-hidden="true"
/>
</button>

<Transition
enter-active-class="transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-none"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition-[opacity,transform] duration-100 ease-in motion-reduce:transition-none"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-if="isOpen"
ref="listboxRef"
role="listbox"
tabindex="0"
:aria-activedescendant="
focusedIndex >= 0 ? `version-${recentVersions[focusedIndex]?.version}` : undefined
"
class="absolute top-full left-0 mt-2 min-w-[180px] bg-bg-elevated border border-border rounded-lg shadow-lg z-50 py-1 max-h-[300px] overflow-y-auto overscroll-contain focus-visible:outline-none"
@keydown="handleListboxKeydown"
>
<NuxtLink
v-for="({ version, tags, isCurrent }, index) in recentVersions"
:id="`version-${version}`"
:key="version"
:to="getDocsUrl(version)"
role="option"
:aria-selected="isCurrent"
:data-focused="index === focusedIndex"
class="flex items-center justify-between gap-3 px-3 py-2 text-sm font-mono hover:bg-bg-muted transition-[color,background-color] focus-visible:outline-none focus-visible:bg-bg-muted"
:class="[
isCurrent ? 'text-fg bg-bg-muted' : 'text-fg-muted',
index === focusedIndex ? 'bg-bg-muted' : '',
]"
@click="isOpen = false"
>
<span class="truncate">{{ version }}</span>
<span v-if="tags.length > 0" class="flex items-center gap-1 shrink-0">
<span
v-for="tag in tags"
:key="tag"
class="text-[10px] px-1.5 py-0.5 rounded font-sans font-medium"
:class="
tag === 'latest'
? 'bg-emerald-500/10 text-emerald-400'
: 'bg-bg-muted text-fg-subtle'
"
>
{{ tag }}
</span>
</span>
</NuxtLink>

<div
v-if="Object.keys(versions).length > MAX_VERSIONS"
class="border-t border-border mt-1 pt-1 px-3 py-2"
>
<NuxtLink
:to="`/${packageName}`"
class="text-xs text-fg-subtle hover:text-fg transition-[color] focus-visible:outline-none focus-visible:text-fg"
@click="isOpen = false"
>
View all {{ Object.keys(versions).length }} versions
</NuxtLink>
</div>
</div>
</Transition>
</div>
</template>
19 changes: 19 additions & 0 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,16 @@ const homepageUrl = computed(() => {
return homepage
})

// Docs URL: use our generated API docs
const docsLink = computed(() => {
if (!displayVersion.value) return null

return {
name: 'docs' as const,
params: { path: [...pkg.value!.name.split('/'), 'v', displayVersion.value.version] },
}
})

function normalizeGitUrl(url: string): string {
return url
.replace(/^git\+/, '')
Expand Down Expand Up @@ -724,6 +734,15 @@ defineOgImageComponent('Package', {
{{ $t('package.links.jsr') }}
</a>
</li>
<li v-if="docsLink">
<NuxtLink
:to="docsLink"
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
>
<span class="i-carbon-document w-4 h-4" aria-hidden="true" />
{{ $t('package.links.docs') }}
</NuxtLink>
</li>
<li v-if="displayVersion" class="sm:ml-auto">
<NuxtLink
:to="{
Expand Down
Loading