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
29 changes: 21 additions & 8 deletions app/components/Package/Playgrounds.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@ import type { PlaygroundLink } from '#shared/types'
import { decodeHtmlEntities } from '~/utils/formatters'

const props = defineProps<{
links: PlaygroundLink[]
packageName: string
requestedVersion: string | null
}>()

// Fetch README for specific version if requested, otherwise latest
const { data } = useLazyFetch<ReadmeResponse>(
() => {
const base = `/api/registry/readme/${props.packageName}`
const version = props.requestedVersion
return version ? `${base}/v/${version}` : base
},
{ default: () => ({ html: '', md: '', playgroundLinks: [], toc: [] }) },
)

const links = computed<PlaygroundLink[]>(() => data.value?.playgroundLinks ?? [])

// Map provider id to icon class
const providerIcons: Record<string, string> = {
'stackblitz': 'i-simple-icons:stackblitz',
Expand Down Expand Up @@ -51,9 +64,9 @@ onClickOutside(dropdownRef, () => {
})

// Single vs multiple
const hasSingleLink = computed(() => props.links.length === 1)
const hasMultipleLinks = computed(() => props.links.length > 1)
const firstLink = computed(() => props.links[0])
const hasSingleLink = computed(() => links.value.length === 1)
const hasMultipleLinks = computed(() => links.value.length > 1)
const firstLink = computed(() => links.value[0])

function closeDropdown() {
isOpen.value = false
Expand All @@ -78,12 +91,12 @@ function handleKeydown(event: KeyboardEvent) {
break
case 'ArrowDown':
event.preventDefault()
focusedIndex.value = (focusedIndex.value + 1) % props.links.length
focusedIndex.value = (focusedIndex.value + 1) % links.value.length
focusMenuItem(focusedIndex.value)
break
case 'ArrowUp':
event.preventDefault()
focusedIndex.value = focusedIndex.value <= 0 ? props.links.length - 1 : focusedIndex.value - 1
focusedIndex.value = focusedIndex.value <= 0 ? links.value.length - 1 : focusedIndex.value - 1
focusMenuItem(focusedIndex.value)
break
case 'Home':
Expand All @@ -93,8 +106,8 @@ function handleKeydown(event: KeyboardEvent) {
break
case 'End':
event.preventDefault()
focusedIndex.value = props.links.length - 1
focusMenuItem(props.links.length - 1)
focusedIndex.value = links.value.length - 1
focusMenuItem(links.value.length - 1)
break
case 'Tab':
closeDropdown()
Expand Down
112 changes: 112 additions & 0 deletions app/components/Package/Readme.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script setup lang="ts">
import Readme from '../Readme.vue'

const props = defineProps<{
packageName: string
requestedVersion: string | null
repositoryUrl: string | null
}>()

// Fetch README for specific version if requested, otherwise latest
const { data, status, error } = useLazyFetch<ReadmeResponse>(() => {

Check failure on line 11 in app/components/Package/Readme.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'error' is declared but its value is never read.
const base = `/api/registry/readme/${props.packageName}`
const version = props.requestedVersion
return version ? `${base}/v/${version}` : base
})

// Track active TOC item based on scroll position
const tocItems = computed(() => data.value?.toc ?? [])
const { activeId: activeTocId } = useActiveTocItem(tocItems)

//copy README file as Markdown
const { copied: copiedReadme, copy: copyReadme } = useClipboard({
source: () => data.value?.md ?? '',
copiedDuring: 2000,
})
</script>

<template>
<div class="flex flex-wrap items-center justify-between mb-3 px-1">
<h2 id="readme-heading" class="group text-xs text-fg-subtle uppercase tracking-wider">
<LinkBase to="#readme">
{{ $t('package.readme.title') }}
</LinkBase>
</h2>
<div class="flex gap-2">
<!-- Copy readme as Markdown button -->
<TooltipApp
v-if="data?.md || status === 'pending' || status === 'idle'"
:text="$t('package.readme.copy_as_markdown')"
position="bottom"
>
<ButtonBase
@click="copyReadme()"
:aria-pressed="copiedReadme"
:aria-label="copiedReadme ? $t('common.copied') : $t('package.readme.copy_as_markdown')"
:classicon="copiedReadme ? 'i-carbon:checkmark' : 'i-simple-icons:markdown'"
:disabled="status === 'pending' || status === 'idle'"
>
{{ copiedReadme ? $t('common.copied') : $t('common.copy') }}
</ButtonBase>
</TooltipApp>
<ReadmeTocDropdown
v-if="data?.toc && data.toc.length > 1"
:toc="data.toc"
:active-id="activeTocId"
:disabled="!data || !data.toc"
/>
<ButtonBase
v-else-if="status === 'pending' || status === 'idle'"
disabled
classicon="i-carbon:list"
block
>
<span class="i-carbon:chevron-down w-3 h-3" aria-hidden="true" />
</ButtonBase>
</div>
</div>

<!-- eslint-disable vue/no-v-html -- HTML is sanitized server-side -->
<Readme v-if="status === 'success' && data?.html" :html="data.html" />
<div class="space-y-4" v-else-if="status === 'pending' || status === 'idle'">
<!-- Heading -->
<SkeletonBlock class="h-7 w-2/3" />
<!-- Paragraphs -->
<SkeletonBlock class="h-4 w-full" />
<SkeletonBlock class="h-4 w-full" />
<SkeletonBlock class="h-4 w-4/5" />
<!-- Gap for section break -->
<SkeletonBlock class="h-6 w-1/2 mt-6" />
<SkeletonBlock class="h-4 w-full" />
<SkeletonBlock class="h-4 w-full" />
<SkeletonBlock class="h-4 w-3/4" />
<!-- Code block placeholder -->
<SkeletonBlock class="h-24 w-full rounded-lg mt-4" />
<SkeletonBlock class="h-4 w-full" />
<SkeletonBlock class="h-4 w-5/6" />
</div>
<div v-else-if="status === 'error'" class="text-red-500">
{{ $t('package.readme.load_error') }}
</div>
<p v-else class="text-fg-muted italic">
{{ $t('package.readme.no_readme') }}
<a
v-if="repositoryUrl"
:href="repositoryUrl"
target="_blank"
rel="noopener noreferrer"
class="link text-fg underline underline-offset-4 decoration-fg-subtle hover:(decoration-fg text-fg) transition-colors duration-200"
>{{ $t('package.readme.view_on_github') }}</a
>
</p>
</template>

<style module>
.areaReadme {
grid-area: readme;
}

.areaReadme > :global(.readme) {
overflow-x: hidden;
}
</style>
1 change: 0 additions & 1 deletion app/components/ReadmeTocDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ function handleKeydown(event: KeyboardEvent) {
@click="toggle"
@keydown="handleKeydown"
classicon="i-carbon:list"
class="px-2.5"
block
>
<span
Expand Down
86 changes: 8 additions & 78 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
PackageVersionInfo,
PackumentVersion,
ProvenanceDetails,
ReadmeResponse,
SkillsListResponse,
} from '#shared/types'
import type { JsrPackageInfo } from '#shared/types/jsr'
Expand Down Expand Up @@ -98,26 +97,6 @@ if (import.meta.server) {
assertValidPackageName(packageName.value)
}

// Fetch README for specific version if requested, otherwise latest
const { data: readmeData } = useLazyFetch<ReadmeResponse>(
() => {
const base = `/api/registry/readme/${packageName.value}`
const version = requestedVersion.value
return version ? `${base}/v/${version}` : base
},
{ default: () => ({ html: '', md: '', playgroundLinks: [], toc: [] }) },
)

//copy README file as Markdown
const { copied: copiedReadme, copy: copyReadme } = useClipboard({
source: () => readmeData.value?.md ?? '',
copiedDuring: 2000,
})

// Track active TOC item based on scroll position
const tocItems = computed(() => readmeData.value?.toc ?? [])
const { activeId: activeTocId } = useActiveTocItem(tocItems)

// Check if package exists on JSR (only for scoped packages)
const { data: jsrInfo } = useLazyFetch<JsrPackageInfo>(() => `/api/jsr/${packageName.value}`, {
default: () => ({ exists: false }),
Expand Down Expand Up @@ -1213,53 +1192,12 @@ const showSkeleton = shallowRef(false)
</ClientOnly>
</div>

<!-- README -->
<section id="readme" class="min-w-0 scroll-mt-20" :class="$style.areaReadme">
<div class="flex flex-wrap items-center justify-between mb-3 px-1">
<h2 id="readme-heading" class="group text-xs text-fg-subtle uppercase tracking-wider">
<LinkBase to="#readme">
{{ $t('package.readme.title') }}
</LinkBase>
</h2>
<div class="flex gap-2">
<!-- Copy readme as Markdown button -->
<TooltipApp
v-if="readmeData?.md"
:text="$t('package.readme.copy_as_markdown')"
position="bottom"
>
<ButtonBase
@click="copyReadme()"
:aria-pressed="copiedReadme"
:aria-label="
copiedReadme ? $t('common.copied') : $t('package.readme.copy_as_markdown')
"
:classicon="copiedReadme ? 'i-carbon:checkmark' : 'i-simple-icons:markdown'"
>
{{ copiedReadme ? $t('common.copied') : $t('common.copy') }}
</ButtonBase>
</TooltipApp>
<ReadmeTocDropdown
v-if="readmeData?.toc && readmeData.toc.length > 1"
:toc="readmeData.toc"
:active-id="activeTocId"
/>
</div>
</div>

<!-- eslint-disable vue/no-v-html -- HTML is sanitized server-side -->
<Readme v-if="readmeData?.html" :html="readmeData.html" />
<p v-else class="text-fg-muted italic">
{{ $t('package.readme.no_readme') }}
<a
v-if="repositoryUrl"
:href="repositoryUrl"
target="_blank"
rel="noopener noreferrer"
class="link text-fg underline underline-offset-4 decoration-fg-subtle hover:(decoration-fg text-fg) transition-colors duration-200"
>{{ $t('package.readme.view_on_github') }}</a
>
</p>
<section id="readme" class="min-w-0 scroll-mt-20">
<PackageReadme
:package-name="pkg.name"
:requested-version="resolvedVersion ?? null"
:repository-url="repositoryUrl"
/>

<section
v-if="hasProvenance(displayVersion) && isMounted"
Expand Down Expand Up @@ -1320,8 +1258,8 @@ const showSkeleton = shallowRef(false)

<!-- Playground links -->
<PackagePlaygrounds
v-if="readmeData?.playgroundLinks?.length"
:links="readmeData.playgroundLinks"
:package-name="pkg.name"
:requested-version="resolvedVersion ?? null"
/>

<PackageCompatibility :engines="displayVersion?.engines" />
Expand Down Expand Up @@ -1471,14 +1409,6 @@ const showSkeleton = shallowRef(false)
overflow-x: hidden;
}

.areaReadme {
grid-area: readme;
}

.areaReadme > :global(.readme) {
overflow-x: hidden;
}

.areaSidebar {
grid-area: sidebar;
}
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
"no_readme": "No README available.",
"view_on_github": "View on GitHub",
"toc_title": "Outline",
"load_error": "Failed to load README",
"callout": {
"note": "Note",
"tip": "Tip",
Expand Down
3 changes: 3 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,9 @@
"toc_title": {
"type": "string"
},
"load_error": {
"type": "string"
},
"callout": {
"type": "object",
"properties": {
Expand Down
1 change: 1 addition & 0 deletions lunaria/files/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
"no_readme": "No README available.",
"view_on_github": "View on GitHub",
"toc_title": "Outline",
"load_error": "Failed to load README",
"callout": {
"note": "Note",
"tip": "Tip",
Expand Down
1 change: 1 addition & 0 deletions lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
"no_readme": "No README available.",
"view_on_github": "View on GitHub",
"toc_title": "Outline",
"load_error": "Failed to load README",
"callout": {
"note": "Note",
"tip": "Tip",
Expand Down
2 changes: 1 addition & 1 deletion server/api/registry/file/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async function fetchFileContent(
* - /api/registry/file/@scope/packageName/v/1.2.3/path/to/file.ts
*/
export default defineCachedEventHandler(
async event => {
async (event): Promise<PackageFileContentResponse> => {
// Parse: [pkg, 'v', version, ...filePath] or [@scope, pkg, 'v', version, ...filePath]
const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []

Expand Down
4 changes: 2 additions & 2 deletions server/api/registry/readme/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async function fetchReadmeFromJsdelivr(
* - /api/registry/readme/@scope/packageName/v/1.2.3 - scoped package, specific version
*/
export default defineCachedEventHandler(
async event => {
async (event): Promise<ReadmeResponse> => {
// Parse package name and optional version from URL segments
// Patterns: [pkg] or [pkg, 'v', version] or [@scope, pkg] or [@scope, pkg, 'v', version]
const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []
Expand Down Expand Up @@ -107,7 +107,7 @@ export default defineCachedEventHandler(
}

if (!readmeContent || readmeContent === NPM_MISSING_README_SENTINEL) {
return { html: '', playgroundLinks: [], toc: [] }
return { html: '', playgroundLinks: [], toc: [], md: '' }
}

// Parse repository info for resolving relative URLs to GitHub
Expand Down
Loading