forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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>