Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 80 additions & 0 deletions app/components/EntrypointSelector.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
import EntrypointSelector from './EntrypointSelector.vue'

const meta = {
component: EntrypointSelector,
} satisfies Meta<typeof EntrypointSelector>

export default meta
type Story = StoryObj<typeof meta>

export const SingleEntrypoint: Story = {
args: {
packageName: 'vue',
version: '3.5.0',
currentEntrypoint: '.',
entrypoints: ['.'],
},
}

export const MultipleEntrypoints: Story = {
args: {
packageName: '@nuxt/kit',
version: '4.3.1',
currentEntrypoint: '.',
entrypoints: ['.', 'compatibility', 'loader'],
},
}

export const ManyEntrypoints: Story = {
args: {
packageName: '@radix-ui/themes',
version: '3.0.0',
currentEntrypoint: 'button',
entrypoints: [
'accordion',
'alert-dialog',
'avatar',
'badge',
'box',
'button',
'callout',
'card',
'checkbox',
'container',
'dialog',
'flex',
'grid',
'heading',
'icon-button',
'inset',
'link',
'popover',
'progress',
'radio-group',
'scroll-area',
'select',
'separator',
'skeleton',
'slider',
'spinner',
'switch',
'table',
'tabs',
'text',
'text-area',
'text-field',
'theme',
'tooltip',
],
},
}

export const NestedEntrypoint: Story = {
args: {
packageName: '@nuxt/kit',
version: '4.3.1',
currentEntrypoint: 'compat/utils',
entrypoints: ['.', 'compat/utils', 'compat/legacy'],
},
}
64 changes: 64 additions & 0 deletions app/components/EntrypointSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script setup lang="ts">
const props = defineProps<{
packageName: string
version: string
currentEntrypoint: string
entrypoints: string[]
}>()

const hasMultiple = computed(() => props.entrypoints.length > 1)
const selectId = useId()

const selectedEntrypoint = shallowRef(props.currentEntrypoint)

watch(
() => props.currentEntrypoint,
entrypoint => {
selectedEntrypoint.value = entrypoint
},
)

const entrypointItems = computed(() =>
props.entrypoints.map(entrypoint => ({
value: entrypoint,
label: formatEntrypointLabel(entrypoint),
})),
)

function formatEntrypointLabel(entrypoint: string): string {
return entrypoint === '.' ? '.' : `./${entrypoint}`
}

function handleEntrypointChange(entrypoint: string | undefined) {
if (!entrypoint || entrypoint === props.currentEntrypoint) {
return
}

selectedEntrypoint.value = entrypoint
navigateTo(docsRoute(props.packageName, props.version, entrypoint))
}
</script>

<template>
<div
v-if="!hasMultiple"
class="text-fg-subtle font-mono text-sm inline-flex items-center gap-1.5"
>
<span class="i-lucide:package w-3.5 h-3.5" aria-hidden="true" />
<span dir="ltr">{{ formatEntrypointLabel(currentEntrypoint) }}</span>
</div>

<div v-else class="inline-flex items-center gap-1.5">
<span class="i-lucide:package w-3.5 h-3.5 text-fg-subtle shrink-0" aria-hidden="true" />
<SelectField
:id="selectId"
v-model="selectedEntrypoint"
:items="entrypointItems"
:label="$t('package.docs.select_entrypoint')"
hidden-label
size="sm"
:select-attrs="{ dir: 'ltr' }"
@update:model-value="handleEntrypointChange"
/>
</div>
</template>
5 changes: 5 additions & 0 deletions app/composables/useCommandPaletteCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const GROUP_ORDER: CommandPaletteGroup[] = [
'settings',
'help',
'npmx',
'entrypoints',
'versions',
]

Expand Down Expand Up @@ -68,6 +69,10 @@ export function useCommandPaletteCommands() {
return packageName
? t('command_palette.groups.versions_with_name', { name: packageName })
: t('command_palette.groups.versions')
case 'entrypoints':
return packageName
? t('command_palette.groups.entrypoints_with_name', { name: packageName })
: t('command_palette.groups.entrypoints')
}
}

Expand Down
51 changes: 51 additions & 0 deletions app/composables/useCommandPaletteEntrypointCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @unocss-include
import type { MaybeRefOrGetter } from 'vue'
import type {
CommandPaletteContextCommandInput,
CommandPalettePackageContext,
} from '~/types/command-palette'

interface EntrypointContext {
packageContext: CommandPalettePackageContext
entrypoints: string[]
currentEntrypoint: string | null
}

function getEntrypointLabel(entrypoint: string): string {
return entrypoint === '.' ? '.' : `./${entrypoint}`
}

export function useCommandPaletteEntrypointCommands(
context: MaybeRefOrGetter<EntrypointContext | null>,
) {
const { t } = useI18n()

useCommandPaletteContextCommands(
computed((): CommandPaletteContextCommandInput[] => {
const ctx = toValue(context)
if (!ctx?.packageContext.resolvedVersion) return []
if (ctx.entrypoints.length === 0) return []

return ctx.entrypoints.map(entrypoint => ({
id: `entrypoint:${entrypoint}`,
group: 'entrypoints' as const,
label: t('command_palette.entrypoint.label', {
entrypoint: getEntrypointLabel(entrypoint),
}),
keywords: [
ctx.packageContext.packageName,
entrypoint,
getEntrypointLabel(entrypoint),
t('command_palette.groups.entrypoints'),
],
iconClass: 'i-lucide:package',
active: entrypoint === ctx.currentEntrypoint,
to: docsRoute(
ctx.packageContext.packageName,
ctx.packageContext.resolvedVersion!,
entrypoint,
),
}))
}),
)
}
83 changes: 61 additions & 22 deletions app/pages/package-docs/[...path].vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { setResponseHeader } from 'h3'
import { parsePackageParam } from '#shared/utils/parse-package-param'

definePageMeta({
name: 'docs',
Expand All @@ -13,24 +14,19 @@ const router = useRouter()
const { t } = useI18n()

const parsedRoute = computed(() => {
const segments = route.params.path?.filter(Boolean)
const vIndex = segments.indexOf('v')

if (vIndex === -1 || vIndex >= segments.length - 1) {
return {
packageName: segments.join('/'),
version: null as string | null,
}
}
const rawPath = route.params.path?.filter(Boolean).join('/') ?? ''
const { packageName, version, rest } = parsePackageParam(rawPath)

return {
packageName: segments.slice(0, vIndex).join('/'),
version: segments.slice(vIndex + 1).join('/'),
packageName,
version: version ?? null,
entrypoint: rest.length > 0 ? rest.join('/') : null,
}
})

const packageName = computed(() => parsedRoute.value.packageName)
const requestedVersion = computed(() => parsedRoute.value.version)
const entrypoint = computed(() => parsedRoute.value.entrypoint)

// Validate package name on server-side for early error detection
if (import.meta.server && packageName.value) {
Expand All @@ -48,12 +44,8 @@ if (import.meta.server && !requestedVersion.value && packageName.value) {
const version = await fetchLatestVersion(packageName.value)
if (version) {
setResponseHeader(useRequestEvent()!, 'Cache-Control', 'no-cache')
const pathSegments = [...packageName.value.split('/'), 'v', version]
app.runWithContext(() =>
navigateTo(
{ name: 'docs', params: { path: pathSegments as [string, ...string[]] } },
{ redirectCode: 302 },
),
navigateTo(docsRoute(packageName.value, version), { redirectCode: 302 }),
)
}
}
Expand All @@ -62,8 +54,7 @@ watch(
[requestedVersion, latestVersion, packageName],
([version, latest, name]) => {
if (!version && latest && name) {
const pathSegments = [...name.split('/'), 'v', latest]
router.replace({ name: 'docs', params: { path: pathSegments as [string, ...string[]] } })
router.replace(docsRoute(name, latest))
}
},
{ immediate: true },
Expand All @@ -90,7 +81,8 @@ useCommandPalettePackageCommands(commandPalettePackageContext)

const docsUrl = computed(() => {
if (!packageName.value || !resolvedVersion.value) return null
return `/api/registry/docs/${packageName.value}/v/${resolvedVersion.value}`
const base = `/api/registry/docs/${packageName.value}/v/${resolvedVersion.value}`
return entrypoint.value ? `${base}/${entrypoint.value}` : base
})

const shouldFetch = computed(() => !!docsUrl.value)
Expand Down Expand Up @@ -119,9 +111,10 @@ const latestVersionDetailed = computed(() => {
return pkg.value.versions[latestTag] ?? null
})

const versionUrlPattern = computed(
() => `/package-docs/${pkg.value?.name || packageName.value}/v/{version}`,
)
const versionUrlPattern = computed(() => {
const base = `/package-docs/${pkg.value?.name || packageName.value}/v/{version}`
return entrypoint.value && entrypoint.value !== '.' ? `${base}/${entrypoint.value}` : base
})

useCommandPaletteVersionCommands(commandPalettePackageContext, versionUrlPattern)

Expand Down Expand Up @@ -171,6 +164,39 @@ const stickyStyle = computed(() => {
'--combined-header-height': `${56 + (packageHeaderHeight.value || 44)}px`,
}
})

// Multi-entrypoint support
const entrypoints = computed(() => docsData.value?.entrypoints ?? null)
const hasRootEntrypoint = computed(() => entrypoints.value?.includes('.') ?? false)
const currentEntrypoint = computed(
() => docsData.value?.entrypoint ?? entrypoint.value ?? (hasRootEntrypoint.value ? '.' : ''),
)

// Redirect to first entrypoint for multi-entrypoint packages
watch(docsData, data => {
if (
data?.entrypoints?.length &&
!data.entrypoints.includes('.') &&
!entrypoint.value &&
resolvedVersion.value
) {
const firstEntrypoint = data.entrypoints[0]!
router.replace(docsRoute(packageName.value, resolvedVersion.value, firstEntrypoint))
}
})

useCommandPaletteEntrypointCommands(
computed(() => {
const packageContext = commandPalettePackageContext.value
const allEntrypoints = entrypoints.value
if (!packageContext || !allEntrypoints?.length || allEntrypoints.length < 2) return null
return {
packageContext,
entrypoints: allEntrypoints,
currentEntrypoint: currentEntrypoint.value || null,
}
}),
)
</script>

<template>
Expand All @@ -184,6 +210,19 @@ const stickyStyle = computed(() => {
page="docs"
/>

<nav
v-if="entrypoints?.length && currentEntrypoint && resolvedVersion"
:aria-label="$t('package.docs.entrypoints')"
class="container py-2 border-b border-border"
>
<EntrypointSelector
:package-name="packageName"
:version="resolvedVersion"
:current-entrypoint="currentEntrypoint"
:entrypoints="entrypoints"
/>
</nav>

<div class="flex" dir="ltr">
<!-- Sidebar TOC -->
<aside
Expand Down
1 change: 1 addition & 0 deletions app/types/command-palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type CommandPaletteGroup =
| 'settings'
| 'npmx'
| 'versions'
| 'entrypoints'

export type CommandPaletteView = 'root' | 'languages' | 'accent-colors' | 'background-themes'

Expand Down
Loading
Loading