From a3ebbd742a712652bd6e2c4cd9f5f618d38c1e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 04:40:50 +0100 Subject: [PATCH 1/4] perf: add explicit return type --- server/api/registry/file/[...pkg].get.ts | 2 +- server/api/registry/readme/[...pkg].get.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/api/registry/file/[...pkg].get.ts b/server/api/registry/file/[...pkg].get.ts index c1dd76c1b..7f2497c40 100644 --- a/server/api/registry/file/[...pkg].get.ts +++ b/server/api/registry/file/[...pkg].get.ts @@ -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 => { // Parse: [pkg, 'v', version, ...filePath] or [@scope, pkg, 'v', version, ...filePath] const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? [] diff --git a/server/api/registry/readme/[...pkg].get.ts b/server/api/registry/readme/[...pkg].get.ts index ca64a55c0..fd54cdfc7 100644 --- a/server/api/registry/readme/[...pkg].get.ts +++ b/server/api/registry/readme/[...pkg].get.ts @@ -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 => { // 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('/') ?? [] From ee73e577b69bb6dea4e59e52dfebf4c30b8a1be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 04:59:31 +0100 Subject: [PATCH 2/4] fix: add missing prop --- server/api/registry/readme/[...pkg].get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/api/registry/readme/[...pkg].get.ts b/server/api/registry/readme/[...pkg].get.ts index fd54cdfc7..82e8e0a08 100644 --- a/server/api/registry/readme/[...pkg].get.ts +++ b/server/api/registry/readme/[...pkg].get.ts @@ -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 From 0d9b25be1216ea6b647bc43fea2c6fc36f0e1b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 05:34:52 +0100 Subject: [PATCH 3/4] refactor: split out package readme component --- app/components/Package/Playgrounds.vue | 29 ++++++--- app/components/Package/Readme.vue | 81 ++++++++++++++++++++++++ app/pages/package/[[org]]/[name].vue | 86 +++----------------------- 3 files changed, 110 insertions(+), 86 deletions(-) create mode 100644 app/components/Package/Readme.vue diff --git a/app/components/Package/Playgrounds.vue b/app/components/Package/Playgrounds.vue index 199439e04..5193e9696 100644 --- a/app/components/Package/Playgrounds.vue +++ b/app/components/Package/Playgrounds.vue @@ -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( + () => { + 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(() => data.value?.playgroundLinks ?? []) + // Map provider id to icon class const providerIcons: Record = { 'stackblitz': 'i-simple-icons:stackblitz', @@ -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 @@ -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': @@ -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() diff --git a/app/components/Package/Readme.vue b/app/components/Package/Readme.vue new file mode 100644 index 000000000..b9367a82e --- /dev/null +++ b/app/components/Package/Readme.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue index 002326d73..8fda50f03 100644 --- a/app/pages/package/[[org]]/[name].vue +++ b/app/pages/package/[[org]]/[name].vue @@ -4,7 +4,6 @@ import type { PackageVersionInfo, PackumentVersion, ProvenanceDetails, - ReadmeResponse, SkillsListResponse, } from '#shared/types' import type { JsrPackageInfo } from '#shared/types/jsr' @@ -98,26 +97,6 @@ if (import.meta.server) { assertValidPackageName(packageName.value) } -// Fetch README for specific version if requested, otherwise latest -const { data: readmeData } = useLazyFetch( - () => { - 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(() => `/api/jsr/${packageName.value}`, { default: () => ({ exists: false }), @@ -1213,53 +1192,12 @@ const showSkeleton = shallowRef(false) - -
-
-

- - {{ $t('package.readme.title') }} - -

-
- - - - {{ copiedReadme ? $t('common.copied') : $t('common.copy') }} - - - -
-
- - - -

- {{ $t('package.readme.no_readme') }} - {{ $t('package.readme.view_on_github') }} -

+
+
@@ -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; } From 66e9fcdf0325c11c075153ebbe1c40008c598721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 09:18:49 +0100 Subject: [PATCH 4/4] feat: add proper loading and error state to readme --- app/components/Package/Readme.vue | 51 ++++++++++++++++++++++------ app/components/ReadmeTocDropdown.vue | 1 - i18n/locales/en.json | 1 + i18n/schema.json | 3 ++ lunaria/files/en-GB.json | 1 + lunaria/files/en-US.json | 1 + 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/app/components/Package/Readme.vue b/app/components/Package/Readme.vue index b9367a82e..3bacadb28 100644 --- a/app/components/Package/Readme.vue +++ b/app/components/Package/Readme.vue @@ -8,14 +8,11 @@ const props = defineProps<{ }>() // Fetch README for specific version if requested, otherwise latest -const { data } = useLazyFetch( - () => { - const base = `/api/registry/readme/${props.packageName}` - const version = props.requestedVersion - return version ? `${base}/v/${version}` : base - }, - { default: () => ({ html: '', md: '', playgroundLinks: [], toc: [] }) }, -) +const { data, status, error } = useLazyFetch(() => { + 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 ?? []) @@ -37,12 +34,17 @@ const { copied: copiedReadme, copy: copyReadme } = useClipboard({
- + {{ copiedReadme ? $t('common.copied') : $t('common.copy') }} @@ -51,12 +53,41 @@ const { copied: copiedReadme, copy: copyReadme } = useClipboard({ v-if="data?.toc && data.toc.length > 1" :toc="data.toc" :active-id="activeTocId" + :disabled="!data || !data.toc" /> + +
- + +
+ + + + + + + + + + + + + + + +
+
+ {{ $t('package.readme.load_error') }} +

{{ $t('package.readme.no_readme') }}