@@ -334,6 +334,68 @@ async function copyInstallCommand() {
334334 setTimeout (() => (copied .value = false ), 2000 )
335335}
336336
337+ // Copy for AI context
338+ const copiedForAI = ref (false )
339+ const aiContextText = computed (() => {
340+ if (! pkg .value || ! displayVersion .value ) return ' '
341+
342+ const parts: string [] = []
343+
344+ // Package header
345+ parts .push (` # ${pkg .value .name }@${displayVersion .value .version } ` )
346+ parts .push (' ' )
347+
348+ // Description
349+ if (pkg .value .description ) {
350+ parts .push (` > ${pkg .value .description } ` )
351+ parts .push (' ' )
352+ }
353+
354+ // Key info
355+ parts .push (' ## Package Info' )
356+ parts .push (` - **Install:** \` ${installCommand .value }\` ` )
357+ if (pkg .value .license ) parts .push (` - **License:** ${pkg .value .license } ` )
358+ if (repositoryUrl .value ) parts .push (` - **Repository:** ${repositoryUrl .value } ` )
359+ if (homepageUrl .value ) parts .push (` - **Homepage:** ${homepageUrl .value } ` )
360+ parts .push (' ' )
361+
362+ // Dependencies summary
363+ // const depCount = getDependencyCount(displayVersion.value)
364+ // if (depCount > 0) {
365+ // parts.push(`## Dependencies (${depCount})`)
366+ // const deps = displayVersion.value.dependencies
367+ // if (deps) {
368+ // parts.push(Object.entries(deps).map(([name, version]) => `- ${name}: ${version}`).join('\n'))
369+ // }
370+ // parts.push('')
371+ // }
372+
373+ // README content (extract text from HTML)
374+ if (readmeData .value ?.html ) {
375+ parts .push (' ## README' )
376+ parts .push (' ' )
377+ // Convert HTML to plain text (client-side only)
378+ if (import .meta .client ) {
379+ const tempDiv = document .createElement (' div' )
380+ tempDiv .innerHTML = readmeData .value .html
381+ // Get text content, preserving some structure
382+ const textContent = tempDiv .innerText || tempDiv .textContent || ' '
383+ parts .push (textContent .trim ())
384+ } else {
385+ parts .push (' [README content available - copy from browser]' )
386+ }
387+ }
388+
389+ return parts .join (' \n ' )
390+ })
391+
392+ async function copyForAI() {
393+ if (! aiContextText .value ) return
394+ await navigator .clipboard .writeText (aiContextText .value )
395+ copiedForAI .value = true
396+ setTimeout (() => (copiedForAI .value = false ), 2000 )
397+ }
398+
337399// Expandable description
338400const descriptionExpanded = ref (false )
339401const descriptionRef = ref <HTMLDivElement >()
@@ -489,33 +551,47 @@ defineOgImageComponent('Package', {
489551 </a >
490552 </div >
491553
492- <!-- Fixed height description container to prevent CLS -->
493- <div ref =" descriptionRef" class =" relative max-w-2xl min-h-[4.5rem]" >
494- <p
495- v-if =" pkg.description"
496- class =" text-fg-muted text-base m-0 overflow-hidden"
497- :class =" descriptionExpanded ? '' : 'max-h-[4.5rem]'"
498- >
499- <MarkdownText :text =" pkg.description" />
500- </p >
501- <p v-else class =" text-fg-subtle text-base m-0 italic" >No description provided</p >
502- <!-- Fade overlay with show more button - only when collapsed and overflowing -->
503- <div
504- v-if =" pkg.description && descriptionOverflows && !descriptionExpanded"
505- class =" absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-bg via-bg/90 to-transparent flex items-end justify-end"
506- >
554+ <!-- Description with Copy for AI button on right (desktop only) -->
555+ <div class =" flex flex-col sm:flex-row sm:items-start gap-4 sm:gap-6" >
556+ <!-- Fixed height description container to prevent CLS -->
557+ <div ref =" descriptionRef" class =" relative flex-1 min-h-[4.5rem]" >
558+ <p
559+ v-if =" pkg.description"
560+ class =" text-fg-muted text-base m-0 overflow-hidden"
561+ :class =" descriptionExpanded ? '' : 'max-h-[4.5rem]'"
562+ >
563+ <MarkdownText :text =" pkg.description" />
564+ </p >
565+ <p v-else class =" text-fg-subtle text-base m-0 italic" >No description provided</p >
566+ <!-- Fade overlay with show more button - only when collapsed and overflowing -->
567+ <div
568+ v-if =" pkg.description && descriptionOverflows && !descriptionExpanded"
569+ class =" absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-bg via-bg/90 to-transparent flex items-end justify-end"
570+ >
571+ <button
572+ type =" button"
573+ class =" font-mono text-xs text-fg-muted hover:text-fg bg-bg px-1 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
574+ aria-label =" Show full description"
575+ @click =" descriptionExpanded = true"
576+ >
577+ show more
578+ </button >
579+ </div >
580+ </div >
581+ <!-- Copy for AI button (hidden on mobile, visible on desktop) -->
582+ <ClientOnly >
507583 <button
508584 type =" button"
509- class =" font-mono text-xs text-fg-muted hover:text-fg bg-bg px-1 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 "
510- aria-label = " Show full description "
511- @click =" descriptionExpanded = true "
585+ class =" btn shrink-0 gap-2 min-w-32 hidden sm:inline-flex "
586+ title = " Copy package info and README for AI context "
587+ @click =" copyForAI "
512588 >
513- show more
589+ <span class =" i-carbon-machine-learning w-4 h-4" aria-hidden =" true" />
590+ <span aria-live =" polite" >{{ copiedForAI ? 'copied!' : 'copy for ai' }}</span >
514591 </button >
515- </div >
592+ </ClientOnly >
516593 </div >
517594 </div >
518-
519595 <div
520596 v-if =" deprecationNotice"
521597 class =" border border-red-400 bg-red-400/10 rounded-lg px-3 py-2 text-base text-red-400"
@@ -809,8 +885,8 @@ defineOgImageComponent('Package', {
809885 <!-- Main package install -->
810886 <div class =" flex items-center gap-2" >
811887 <span class =" text-fg-subtle font-mono text-sm select-none" >$</span >
812- <code class =" font-mono text-sm"
813- > <ClientOnly
888+ <code class =" font-mono text-sm" >
889+ <ClientOnly
814890 ><span
815891 v-for =" (part, i) in installCommandParts"
816892 :key =" i"
@@ -820,8 +896,8 @@ defineOgImageComponent('Package', {
820896 ><span class =" text-fg" >npm</span
821897 ><span class =" text-fg-muted" > install {{ pkg.name }}</span ></template
822898 ></ClientOnly
823- ></ code
824- >
899+ >
900+ </ code >
825901 </div >
826902 <!-- @types package install (when enabled) -->
827903 <div v-if =" showTypesInInstall" class =" flex items-center gap-2" >
@@ -862,9 +938,22 @@ defineOgImageComponent('Package', {
862938 <!-- Main content (README) -->
863939 <div class =" lg:col-span-2 order-2 lg:order-1 min-w-0" >
864940 <section aria-labelledby =" readme-heading" >
865- <h2 id =" readme-heading" class =" text-xs text-fg-subtle uppercase tracking-wider mb-4" >
866- Readme
867- </h2 >
941+ <div class =" flex items-center justify-between mb-4" >
942+ <h2 id =" readme-heading" class =" text-xs text-fg-subtle uppercase tracking-wider" >
943+ Readme
944+ </h2 >
945+ <ClientOnly >
946+ <button
947+ type =" button"
948+ class =" btn-ghost gap-1.5 text-xs"
949+ title =" Copy package info and README for AI context"
950+ @click =" copyForAI"
951+ >
952+ <span class =" i-carbon-machine-learning w-3.5 h-3.5" aria-hidden =" true" />
953+ <span aria-live =" polite" >{{ copiedForAI ? 'copied!' : 'copy for ai' }}</span >
954+ </button >
955+ </ClientOnly >
956+ </div >
868957 <!-- eslint-disable vue/no-v-html -- HTML is sanitized server-side -->
869958 <div
870959 v-if =" readmeData?.html"
0 commit comments