[READ-ONLY] a fast, modern browser for the npm registry
at main 246 lines 9.7 kB view raw
1<script setup lang="ts"> 2import type { SkillListItem } from '#shared/types' 3 4const props = defineProps<{ 5 skills: SkillListItem[] 6 packageName: string 7 version?: string 8}>() 9 10function getSkillSourceUrl(skill: SkillListItem): string { 11 const base = `/package-code/${props.packageName}` 12 const versionPath = props.version ? `/v/${props.version}` : '' 13 return `${base}${versionPath}/skills/${skill.dirName}/SKILL.md` 14} 15 16const expandedSkills = ref<Set<string>>(new Set()) 17 18function toggleSkill(dirName: string) { 19 if (expandedSkills.value.has(dirName)) { 20 expandedSkills.value.delete(dirName) 21 } else { 22 expandedSkills.value.add(dirName) 23 } 24 expandedSkills.value = new Set(expandedSkills.value) 25} 26 27type InstallMethod = 'skills-npm' | 'skills-cli' 28const selectedMethod = ref<InstallMethod>('skills-npm') 29 30const baseUrl = computed(() => 31 typeof window !== 'undefined' ? window.location.origin : 'https://npmx.dev', 32) 33 34const installCommand = computed(() => { 35 if (!props.skills.length) return null 36 return `npx skills add ${baseUrl.value}/${props.packageName}` 37}) 38 39const { copied, copy } = useClipboard({ copiedDuring: 2000 }) 40const copyCommand = () => installCommand.value && copy(installCommand.value) 41 42function getWarningTooltip(skill: SkillListItem): string | undefined { 43 if (!skill.warnings?.length) return undefined 44 return skill.warnings.map(w => w.message).join(', ') 45} 46</script> 47 48<template> 49 <Modal :modal-title="$t('package.skills.title')" id="skills-modal" class="sm:max-w-2xl"> 50 <!-- Install header with tabs --> 51 <div class="flex flex-wrap items-center justify-between gap-2 mb-3"> 52 <h3 class="text-xs text-fg-subtle uppercase tracking-wider"> 53 {{ $t('package.skills.install') }} 54 </h3> 55 <div 56 class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border-subtle rounded-md" 57 role="tablist" 58 :aria-label="$t('package.skills.installation_method')" 59 > 60 <button 61 role="tab" 62 :aria-selected="selectedMethod === 'skills-npm'" 63 :tabindex="selectedMethod === 'skills-npm' ? 0 : -1" 64 type="button" 65 class="px-2 py-1 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-accent/70" 66 :class=" 67 selectedMethod === 'skills-npm' 68 ? 'bg-bg border-border shadow-sm text-fg' 69 : 'border-transparent text-fg-subtle hover:text-fg' 70 " 71 @click="selectedMethod = 'skills-npm'" 72 > 73 skills-npm 74 </button> 75 <button 76 role="tab" 77 :aria-selected="selectedMethod === 'skills-cli'" 78 :tabindex="selectedMethod === 'skills-cli' ? 0 : -1" 79 type="button" 80 class="px-2 py-1 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-accent/70" 81 :class=" 82 selectedMethod === 'skills-cli' 83 ? 'bg-bg border-border shadow-sm text-fg' 84 : 'border-transparent text-fg-subtle hover:text-fg' 85 " 86 @click="selectedMethod = 'skills-cli'" 87 > 88 skills CLI 89 </button> 90 </div> 91 </div> 92 93 <!-- skills-npm: compatible --> 94 <div 95 v-if="selectedMethod === 'skills-npm'" 96 class="flex items-center justify-between gap-2 px-3 py-2.5 sm:px-4 bg-bg-subtle border border-border rounded-lg mb-5" 97 > 98 <i18n-t 99 keypath="package.skills.compatible_with" 100 tag="span" 101 class="text-sm text-fg-muted" 102 scope="global" 103 > 104 <template #tool> 105 <code class="font-mono text-fg">skills-npm</code> 106 </template> 107 </i18n-t> 108 <a 109 href="/package/skills-npm" 110 class="inline-flex items-center gap-1 text-xs text-fg-subtle hover:text-fg transition-colors shrink-0" 111 > 112 {{ $t('package.skills.learn_more') }} 113 <span class="i-lucide:arrow-right w-3 h-3" /> 114 </a> 115 </div> 116 117 <!-- skills CLI: terminal command --> 118 <div 119 v-else-if="installCommand" 120 class="bg-bg-subtle border border-border rounded-lg overflow-hidden mb-5" 121 > 122 <div class="flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3"> 123 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" /> 124 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" /> 125 <span class="w-2.5 h-2.5 rounded-full bg-fg-subtle" /> 126 </div> 127 <div class="px-3 pt-2 pb-3 sm:px-4 sm:pt-3 sm:pb-4 overflow-x-auto"> 128 <div class="relative group/cmd"> 129 <code class="font-mono text-sm whitespace-nowrap"> 130 <span class="text-fg-subtle select-none">$ </span> 131 <span class="text-fg">npx </span> 132 <span class="text-fg-muted">skills add {{ baseUrl }}/{{ packageName }}</span> 133 </code> 134 <button 135 type="button" 136 class="absolute top-0 inset-ie-0 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/cmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70" 137 :aria-label="$t('package.get_started.copy_command')" 138 @click.stop="copyCommand" 139 > 140 <span aria-live="polite">{{ copied ? $t('common.copied') : $t('common.copy') }}</span> 141 </button> 142 </div> 143 </div> 144 </div> 145 146 <!-- Skills list --> 147 <div class="flex items-baseline justify-between gap-2 mb-2"> 148 <h3 class="text-xs text-fg-subtle uppercase tracking-wider"> 149 {{ $t('package.skills.available_skills') }} 150 </h3> 151 <span class="text-xs text-fg-subtle/60">{{ $t('package.skills.click_to_expand') }}</span> 152 </div> 153 <ul class="space-y-0.5 list-none m-0 p-0"> 154 <li v-for="skill in skills" :key="skill.dirName"> 155 <button 156 type="button" 157 class="w-full flex items-center gap-2 py-1.5 text-start rounded transition-colors hover:bg-bg-subtle focus-visible:outline-accent/70" 158 :aria-expanded="expandedSkills.has(skill.dirName)" 159 @click="toggleSkill(skill.dirName)" 160 > 161 <span 162 class="i-lucide:chevron-right w-3 h-3 text-fg-subtle shrink-0 transition-transform duration-200" 163 :class="{ 'rotate-90': expandedSkills.has(skill.dirName) }" 164 aria-hidden="true" 165 /> 166 <span class="font-mono text-sm text-fg-muted">{{ skill.name }}</span> 167 <TooltipApp 168 v-if="skill.warnings?.length" 169 class="shrink-0 p-2 -m-2" 170 aria-hidden="true" 171 :text="getWarningTooltip(skill)" 172 to="#skills-modal" 173 defer 174 > 175 <span class="i-lucide:circle-alert w-3.5 h-3.5 text-amber-500" /> 176 </TooltipApp> 177 </button> 178 179 <!-- Expandable details --> 180 <div 181 class="grid transition-[grid-template-rows] duration-200 ease-out" 182 :class="expandedSkills.has(skill.dirName) ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'" 183 > 184 <div class="overflow-hidden"> 185 <div class="ps-5.5 pe-2 pb-2 pt-1 space-y-1.5"> 186 <!-- Description --> 187 <p v-if="skill.description" class="text-sm text-fg-subtle"> 188 {{ skill.description }} 189 </p> 190 <p v-else class="text-sm text-fg-subtle/50 italic"> 191 {{ $t('package.skills.no_description') }} 192 </p> 193 194 <!-- File counts & warnings --> 195 <div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs"> 196 <span v-if="skill.fileCounts?.scripts" class="text-fg-subtle"> 197 <span class="i-lucide:file-code size-3 align-[-2px] me-0.5" />{{ 198 $t( 199 'package.skills.file_counts.scripts', 200 { count: skill.fileCounts.scripts }, 201 skill.fileCounts.scripts, 202 ) 203 }} 204 </span> 205 <span v-if="skill.fileCounts?.references" class="text-fg-subtle"> 206 <span class="i-lucide:file-text size-3 align-[-2px] me-0.5" />{{ 207 $t( 208 'package.skills.file_counts.refs', 209 { count: skill.fileCounts.references }, 210 skill.fileCounts.references, 211 ) 212 }} 213 </span> 214 <span v-if="skill.fileCounts?.assets" class="text-fg-subtle"> 215 <span class="i-lucide:image size-3 align-[-2px] me-0.5" />{{ 216 $t( 217 'package.skills.file_counts.assets', 218 { count: skill.fileCounts.assets }, 219 skill.fileCounts.assets, 220 ) 221 }} 222 </span> 223 <template v-for="warning in skill.warnings" :key="warning.message"> 224 <span class="text-amber-500"> 225 <span class="i-lucide:circle-alert size-3 align-[-2px] me-0.5" />{{ 226 warning.message 227 }} 228 </span> 229 </template> 230 </div> 231 232 <!-- Source link --> 233 <NuxtLink 234 :to="getSkillSourceUrl(skill)" 235 class="inline-flex items-center gap-1 text-xs text-fg-subtle hover:text-fg transition-colors" 236 @click.stop 237 > 238 <span class="i-lucide:code size-3" />{{ $t('package.skills.view_source') }} 239 </NuxtLink> 240 </div> 241 </div> 242 </div> 243 </li> 244 </ul> 245 </Modal> 246</template>