[READ-ONLY] a fast, modern browser for the npm registry
at main 339 lines 14 kB view raw
1<script setup lang="ts"> 2import type { JsrPackageInfo } from '#shared/types/jsr' 3import type { DevDependencySuggestion } from '#shared/utils/dev-dependency' 4import type { PackageManagerId } from '~/utils/install-command' 5 6const props = defineProps<{ 7 packageName: string 8 requestedVersion?: string | null 9 installVersionOverride?: string | null 10 jsrInfo?: JsrPackageInfo | null 11 devDependencySuggestion?: DevDependencySuggestion | null 12 typesPackageName?: string | null 13 executableInfo?: { hasExecutable: boolean; primaryCommand?: string } | null 14 createPackageInfo?: { packageName: string } | null 15}>() 16 17const { selectedPM, showTypesInInstall, copied, copyInstallCommand } = useInstallCommand( 18 () => props.packageName, 19 () => props.requestedVersion ?? null, 20 () => props.jsrInfo ?? null, 21 () => props.typesPackageName ?? null, 22 () => props.installVersionOverride ?? null, 23) 24 25// Generate install command parts for a specific package manager 26function getInstallPartsForPM(pmId: PackageManagerId) { 27 return getInstallCommandParts({ 28 packageName: props.packageName, 29 packageManager: pmId, 30 version: props.installVersionOverride ?? props.requestedVersion, 31 jsrInfo: props.jsrInfo, 32 }) 33} 34 35const devDependencySuggestion = computed( 36 () => props.devDependencySuggestion ?? { recommended: false as const }, 37) 38 39function getDevInstallPartsForPM(pmId: PackageManagerId) { 40 return getInstallCommandParts({ 41 packageName: props.packageName, 42 packageManager: pmId, 43 version: props.requestedVersion, 44 jsrInfo: props.jsrInfo, 45 dev: true, 46 }) 47} 48 49// Generate run command parts for a specific package manager 50function getRunPartsForPM(pmId: PackageManagerId, command?: string) { 51 return getRunCommandParts({ 52 packageName: props.packageName, 53 packageManager: pmId, 54 jsrInfo: props.jsrInfo, 55 command, 56 isBinaryOnly: false, 57 }) 58} 59 60// Generate create command parts for a specific package manager 61function getCreatePartsForPM(pmId: PackageManagerId) { 62 if (!props.createPackageInfo) return [] 63 const pm = packageManagers.find(p => p.id === pmId) 64 if (!pm) return [] 65 66 const createPkgName = props.createPackageInfo.packageName 67 let shortName: string 68 if (createPkgName.startsWith('@')) { 69 const slashIndex = createPkgName.indexOf('/') 70 const name = createPkgName.slice(slashIndex + 1) 71 shortName = name.startsWith('create-') ? name.slice('create-'.length) : name 72 } else { 73 shortName = createPkgName.startsWith('create-') 74 ? createPkgName.slice('create-'.length) 75 : createPkgName 76 } 77 78 return [...pm.create.split(' '), shortName] 79} 80 81// Generate @types install command parts for a specific package manager 82function getTypesInstallPartsForPM(pmId: PackageManagerId) { 83 if (!props.typesPackageName) return [] 84 const pm = packageManagers.find(p => p.id === pmId) 85 if (!pm) return [] 86 87 const devFlag = getDevDependencyFlag(pmId) 88 const pkgSpec = pmId === 'deno' ? `npm:${props.typesPackageName}` : props.typesPackageName 89 90 return [pm.label, pm.action, devFlag, pkgSpec] 91} 92 93// Full run command for copying (uses current selected PM) 94function getFullRunCommand(command?: string) { 95 return getRunCommand({ 96 packageName: props.packageName, 97 packageManager: selectedPM.value, 98 jsrInfo: props.jsrInfo, 99 command, 100 }) 101} 102 103// Full create command for copying (uses current selected PM) 104function getFullCreateCommand() { 105 return getCreatePartsForPM(selectedPM.value).join(' ') 106} 107 108// Copy handlers 109const { copied: runCopied, copy: copyRun } = useClipboard({ copiedDuring: 2000 }) 110const copyRunCommand = (command?: string) => copyRun(getFullRunCommand(command)) 111 112const { copied: createCopied, copy: copyCreate } = useClipboard({ copiedDuring: 2000 }) 113const copyCreateCommand = () => copyCreate(getFullCreateCommand()) 114 115const { copied: devInstallCopied, copy: copyDevInstall } = useClipboard({ copiedDuring: 2000 }) 116const copyDevInstallCommand = () => 117 copyDevInstall( 118 getInstallCommand({ 119 packageName: props.packageName, 120 packageManager: selectedPM.value, 121 version: props.requestedVersion, 122 jsrInfo: props.jsrInfo, 123 dev: true, 124 }), 125 ) 126</script> 127 128<template> 129 <div class="relative group"> 130 <!-- Terminal-style install command --> 131 <div class="bg-bg-subtle border border-border rounded-lg overflow-hidden"> 132 <div class="flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3"> 133 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" /> 134 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" /> 135 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" /> 136 </div> 137 <div class="px-3 pt-2 pb-3 sm:px-4 sm:pt-3 sm:pb-4 space-y-1 overflow-x-auto" dir="ltr"> 138 <!-- Install command - render all PM variants, CSS controls visibility --> 139 <div 140 v-for="pm in packageManagers" 141 :key="`install-${pm.id}`" 142 :data-pm-cmd="pm.id" 143 class="flex items-center gap-2 group/installcmd min-w-0" 144 > 145 <span class="self-start text-fg-subtle font-mono text-sm select-none shrink-0">$</span> 146 <code class="font-mono text-sm min-w-0" 147 ><span 148 v-for="(part, i) in getInstallPartsForPM(pm.id)" 149 :key="i" 150 :class="i === 0 ? 'text-fg' : 'text-fg-muted'" 151 >{{ i > 0 ? ' ' : '' }}{{ part }}</span 152 ></code 153 > 154 <button 155 type="button" 156 class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/installcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none" 157 :aria-label="$t('package.get_started.copy_command')" 158 @click.stop="copyInstallCommand" 159 > 160 <span aria-live="polite">{{ copied ? $t('common.copied') : $t('common.copy') }}</span> 161 </button> 162 </div> 163 164 <!-- Suggested dev dependency install command --> 165 <template v-if="devDependencySuggestion.recommended"> 166 <div class="flex items-center gap-2 pt-1 select-none"> 167 <span class="text-fg-subtle font-mono text-sm" 168 ># {{ $t('package.get_started.dev_dependency_hint') }}</span 169 > 170 </div> 171 <div 172 v-for="pm in packageManagers" 173 :key="`install-dev-${pm.id}`" 174 :data-pm-cmd="pm.id" 175 class="flex items-center gap-2 group/devinstallcmd min-w-0" 176 > 177 <span class="text-fg-subtle font-mono text-sm select-none shrink-0">$</span> 178 <code class="font-mono text-sm min-w-0" 179 ><span 180 v-for="(part, i) in getDevInstallPartsForPM(pm.id)" 181 :key="i" 182 :class="i === 0 ? 'text-fg' : 'text-fg-muted'" 183 >{{ i > 0 ? ' ' : '' }}{{ part }}</span 184 ></code 185 > 186 <ButtonBase 187 type="button" 188 size="small" 189 class="text-fg-muted bg-bg-subtle/80 border-border opacity-0 group-hover/devinstallcmd:opacity-100 active:scale-95 focus-visible:opacity-100 select-none" 190 :aria-label="$t('package.get_started.copy_dev_command')" 191 @click.stop="copyDevInstallCommand" 192 > 193 <span aria-live="polite">{{ 194 devInstallCopied ? $t('common.copied') : $t('common.copy') 195 }}</span> 196 </ButtonBase> 197 </div> 198 </template> 199 200 <!-- @types package install - render all PM variants when types package exists --> 201 <template v-if="typesPackageName && showTypesInInstall"> 202 <div 203 v-for="pm in packageManagers" 204 :key="`types-${pm.id}`" 205 :data-pm-cmd="pm.id" 206 class="flex items-center gap-2 min-w-0" 207 > 208 <span class="self-start text-fg-subtle font-mono text-sm select-none shrink-0">$</span> 209 <code class="font-mono text-sm min-w-0" 210 ><span 211 v-for="(part, i) in getTypesInstallPartsForPM(pm.id)" 212 :key="i" 213 :class="i === 0 ? 'text-fg' : 'text-fg-muted'" 214 >{{ i > 0 ? ' ' : '' }}{{ part }}</span 215 ></code 216 > 217 <NuxtLink 218 :to="packageRoute(typesPackageName!)" 219 class="text-fg-subtle hover:text-fg-muted text-xs transition-colors focus-visible:outline-accent/70 rounded select-none" 220 :title="$t('package.get_started.view_types', { package: typesPackageName })" 221 > 222 <span class="i-lucide:arrow-right rtl-flip w-3 h-3 align-middle" aria-hidden="true" /> 223 <span class="sr-only">View {{ typesPackageName }}</span> 224 </NuxtLink> 225 </div> 226 </template> 227 228 <!-- Run command (only if package has executables) - render all PM variants --> 229 <template v-if="executableInfo?.hasExecutable"> 230 <!-- Comment line --> 231 <div class="flex items-center gap-2 pt-1" dir="auto"> 232 <span class="text-fg-subtle font-mono text-sm select-none" 233 ># {{ $t('package.run.locally') }}</span 234 > 235 </div> 236 237 <div 238 v-for="pm in packageManagers" 239 :key="`run-${pm.id}`" 240 :data-pm-cmd="pm.id" 241 class="flex items-center gap-2 group/runcmd" 242 > 243 <span class="self-start text-fg-subtle font-mono text-sm select-none">$</span> 244 <code class="font-mono text-sm" 245 ><span 246 v-for="(part, i) in getRunPartsForPM(pm.id, executableInfo?.primaryCommand)" 247 :key="i" 248 :class="i === 0 ? 'text-fg' : 'text-fg-muted'" 249 >{{ i > 0 ? ' ' : '' }}{{ part }}</span 250 ></code 251 > 252 <button 253 type="button" 254 class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/runcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none" 255 @click.stop="copyRunCommand(executableInfo?.primaryCommand)" 256 > 257 {{ runCopied ? $t('common.copied') : $t('common.copy') }} 258 </button> 259 </div> 260 </template> 261 262 <!-- Create command (for packages with associated create-* package) - render all PM variants --> 263 <template v-if="createPackageInfo"> 264 <!-- Comment line --> 265 <div class="flex items-center gap-2 pt-1 select-none" dir="auto"> 266 <span class="text-fg-subtle font-mono text-sm"># {{ $t('package.create.title') }}</span> 267 <TooltipApp 268 :text="$t('package.create.view', { packageName: createPackageInfo.packageName })" 269 > 270 <NuxtLink 271 :to="packageRoute(createPackageInfo.packageName)" 272 class="inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1 text-fg-muted hover:text-fg text-xs transition-colors focus-visible:outline-2 focus-visible:outline-accent/70 rounded" 273 > 274 <span class="i-lucide:info w-3 h-3" aria-hidden="true" /> 275 <span class="sr-only">{{ 276 $t('package.create.view', { packageName: createPackageInfo.packageName }) 277 }}</span> 278 </NuxtLink> 279 </TooltipApp> 280 </div> 281 282 <div 283 v-for="pm in packageManagers" 284 :key="`create-${pm.id}`" 285 :data-pm-cmd="pm.id" 286 class="flex items-center gap-2 group/createcmd" 287 > 288 <span class="self-start text-fg-subtle font-mono text-sm select-none">$</span> 289 <code class="font-mono text-sm" 290 ><span 291 v-for="(part, i) in getCreatePartsForPM(pm.id)" 292 :key="i" 293 :class="i === 0 ? 'text-fg' : 'text-fg-muted'" 294 >{{ i > 0 ? ' ' : '' }}{{ part }}</span 295 ></code 296 > 297 <button 298 type="button" 299 class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/createcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none" 300 :aria-label="$t('package.create.copy_command')" 301 @click.stop="copyCreateCommand" 302 > 303 <span aria-live="polite">{{ 304 createCopied ? $t('common.copied') : $t('common.copy') 305 }}</span> 306 </button> 307 </div> 308 </template> 309 </div> 310 </div> 311 </div> 312</template> 313 314<style> 315/* 316 * Package manager command visibility based on data-pm attribute on <html>. 317 * All variants are rendered; CSS shows only the selected one. 318 */ 319 320/* Hide all variants by default when preference is set */ 321:root[data-pm] [data-pm-cmd] { 322 display: none; 323} 324 325/* Show only the matching package manager command */ 326:root[data-pm='npm'] [data-pm-cmd='npm'], 327:root[data-pm='pnpm'] [data-pm-cmd='pnpm'], 328:root[data-pm='yarn'] [data-pm-cmd='yarn'], 329:root[data-pm='bun'] [data-pm-cmd='bun'], 330:root[data-pm='deno'] [data-pm-cmd='deno'], 331:root[data-pm='vlt'] [data-pm-cmd='vlt'] { 332 display: flex; 333} 334 335/* Fallback: when no data-pm is set (SSR initial), show npm as default */ 336:root:not([data-pm]) [data-pm-cmd]:not([data-pm-cmd='npm']) { 337 display: none; 338} 339</style>