[READ-ONLY] a fast, modern browser for the npm registry
at main 294 lines 12 kB view raw
1<script setup lang="ts"> 2import type { Role } from '#server/api/contributors.get' 3 4const router = useRouter() 5const canGoBack = useCanGoBack() 6 7useSeoMeta({ 8 title: () => `${$t('about.title')} - npmx`, 9 ogTitle: () => `${$t('about.title')} - npmx`, 10 twitterTitle: () => `${$t('about.title')} - npmx`, 11 description: () => $t('about.meta_description'), 12 ogDescription: () => $t('about.meta_description'), 13 twitterDescription: () => $t('about.meta_description'), 14}) 15 16defineOgImageComponent('Default', { 17 primaryColor: '#60a5fa', 18 title: 'about npmx', 19 description: 'a fast, modern browser for the **npm registry**', 20}) 21 22const pmLinks = { 23 npm: 'https://www.npmjs.com/', 24 pnpm: 'https://pnpm.io/', 25 yarn: 'https://yarnpkg.com/', 26 bun: 'https://bun.sh/', 27 deno: 'https://deno.com/', 28 vlt: 'https://www.vlt.sh/', 29} 30 31const { data: contributors, status: contributorsStatus } = useLazyFetch('/api/contributors') 32 33const governanceMembers = computed( 34 () => contributors.value?.filter(c => c.role !== 'contributor') ?? [], 35) 36 37const communityContributors = computed( 38 () => contributors.value?.filter(c => c.role === 'contributor') ?? [], 39) 40 41const roleLabels = computed( 42 () => 43 ({ 44 steward: $t('about.team.role_steward'), 45 maintainer: $t('about.team.role_maintainer'), 46 }) as Partial<Record<Role, string>>, 47) 48</script> 49 50<template> 51 <main class="container flex-1 py-12 sm:py-16 overflow-x-hidden"> 52 <article class="max-w-2xl mx-auto"> 53 <header class="mb-12"> 54 <div class="flex items-baseline justify-between gap-4 mb-4"> 55 <h1 class="font-mono text-3xl sm:text-4xl font-medium"> 56 {{ $t('about.heading') }} 57 </h1> 58 <button 59 type="button" 60 class="cursor-pointer inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0" 61 @click="router.back()" 62 v-if="canGoBack" 63 > 64 <span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" /> 65 <span class="hidden sm:inline">{{ $t('nav.back') }}</span> 66 </button> 67 </div> 68 <p class="text-fg-muted text-lg"> 69 {{ $t('tagline') }} 70 </p> 71 </header> 72 73 <section class="prose prose-invert max-w-none space-y-8"> 74 <div> 75 <h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4"> 76 {{ $t('about.what_we_are.title') }} 77 </h2> 78 <p class="text-fg-muted leading-relaxed mb-4"> 79 <i18n-t keypath="about.what_we_are.description" tag="span" scope="global"> 80 <template #betterUxDx> 81 <strong class="text-fg">{{ $t('about.what_we_are.better_ux_dx') }}</strong> 82 </template> 83 <template #jsr> 84 <LinkBase to="https://jsr.io/">JSR</LinkBase> 85 </template> 86 </i18n-t> 87 </p> 88 <p class="text-fg-muted leading-relaxed"> 89 <i18n-t keypath="about.what_we_are.admin_description" tag="span" scope="global"> 90 <template #adminUi> 91 <strong class="text-fg">{{ $t('about.what_we_are.admin_ui') }}</strong> 92 </template> 93 </i18n-t> 94 </p> 95 </div> 96 97 <div> 98 <h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4"> 99 {{ $t('about.what_we_are_not.title') }} 100 </h2> 101 <ul class="space-y-3 text-fg-muted list-none p-0"> 102 <li class="flex items-start gap-3"> 103 <span class="text-fg-subtle shrink-0 mt-1">&mdash;</span> 104 <span> 105 <strong class="text-fg">{{ 106 $t('about.what_we_are_not.not_package_manager') 107 }}</strong> 108 {{ ' ' }} 109 <i18n-t 110 keypath="about.what_we_are_not.package_managers_exist" 111 tag="span" 112 scope="global" 113 > 114 <template #already>{{ $t('about.what_we_are_not.words.already') }}</template> 115 <template #people> 116 <LinkBase :to="pmLinks.npm" class="font-sans">{{ 117 $t('about.what_we_are_not.words.people') 118 }}</LinkBase> 119 </template> 120 <template #building> 121 <LinkBase :to="pmLinks.pnpm" class="font-sans">{{ 122 $t('about.what_we_are_not.words.building') 123 }}</LinkBase> 124 </template> 125 <template #really> 126 <LinkBase :to="pmLinks.yarn" class="font-sans">{{ 127 $t('about.what_we_are_not.words.really') 128 }}</LinkBase> 129 </template> 130 <template #cool> 131 <LinkBase :to="pmLinks.bun" class="font-sans">{{ 132 $t('about.what_we_are_not.words.cool') 133 }}</LinkBase> 134 </template> 135 <template #package> 136 <LinkBase :to="pmLinks.deno" class="font-sans">{{ 137 $t('about.what_we_are_not.words.package') 138 }}</LinkBase> 139 </template> 140 <template #managers> 141 <LinkBase :to="pmLinks.vlt" class="font-sans">{{ 142 $t('about.what_we_are_not.words.managers') 143 }}</LinkBase> 144 </template> 145 </i18n-t> 146 </span> 147 </li> 148 <li class="flex items-start gap-3"> 149 <span class="text-fg-subtle shrink-0 mt-1">&mdash;</span> 150 <span> 151 <strong class="text-fg">{{ $t('about.what_we_are_not.not_registry') }}</strong> 152 {{ $t('about.what_we_are_not.registry_description') }} 153 </span> 154 </li> 155 </ul> 156 </div> 157 158 <div> 159 <h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4"> 160 {{ $t('about.team.title') }} 161 </h2> 162 <p class="text-fg-muted leading-relaxed mb-6"> 163 {{ $t('about.contributors.description') }} 164 </p> 165 166 <!-- Governance: stewards + maintainers --> 167 <section 168 v-if="governanceMembers.length" 169 class="mb-12" 170 aria-labelledby="governance-heading" 171 > 172 <h3 173 id="governance-heading" 174 class="text-sm text-fg-subtle uppercase tracking-wider mb-4" 175 > 176 {{ $t('about.team.governance') }} 177 </h3> 178 179 <ul class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 list-none p-0"> 180 <li 181 v-for="person in governanceMembers" 182 :key="person.id" 183 class="relative flex items-center gap-3 p-3 border border-border rounded-lg hover:border-border-hover hover:bg-bg-muted transition-[border-color,background-color] duration-200 cursor-pointer focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50" 184 > 185 <img 186 :src="`${person.avatar_url}&s=80`" 187 :alt="`${person.login}'s avatar`" 188 class="w-12 h-12 rounded-md ring-1 ring-border shrink-0" 189 loading="lazy" 190 /> 191 <div class="min-w-0 flex-1"> 192 <div class="font-mono text-sm text-fg truncate"> 193 <NuxtLink 194 :to="person.html_url" 195 target="_blank" 196 class="decoration-none after:content-[''] after:absolute after:inset-0" 197 :aria-label="$t('about.contributors.view_profile', { name: person.login })" 198 > 199 @{{ person.login }} 200 </NuxtLink> 201 </div> 202 <div class="text-xs text-fg-muted tracking-tight"> 203 {{ roleLabels[person.role] ?? person.role }} 204 </div> 205 <LinkBase 206 v-if="person.sponsors_url" 207 :to="person.sponsors_url" 208 no-underline 209 no-external-icon 210 classicon="i-lucide:heart" 211 class="relative z-10 text-xs text-fg-muted hover:text-pink-400 mt-0.5" 212 :aria-label="$t('about.team.sponsor_aria', { name: person.login })" 213 > 214 {{ $t('about.team.sponsor') }} 215 </LinkBase> 216 </div> 217 <span 218 class="i-lucide:external-link rtl-flip w-3.5 h-3.5 text-fg-muted opacity-50 shrink-0 self-start mt-0.5" 219 aria-hidden="true" 220 /> 221 </li> 222 </ul> 223 </section> 224 225 <!-- Contributors cloud --> 226 <section aria-labelledby="contributors-heading"> 227 <h3 228 id="contributors-heading" 229 class="text-sm text-fg-subtle uppercase tracking-wider mb-4" 230 > 231 {{ 232 $t( 233 'about.contributors.title', 234 { count: $n(communityContributors.length) }, 235 communityContributors.length, 236 ) 237 }} 238 </h3> 239 240 <div 241 v-if="contributorsStatus === 'pending'" 242 class="text-fg-subtle text-sm" 243 role="status" 244 > 245 {{ $t('about.contributors.loading') }} 246 </div> 247 <div 248 v-else-if="contributorsStatus === 'error'" 249 class="text-fg-subtle text-sm" 250 role="alert" 251 > 252 {{ $t('about.contributors.error') }} 253 </div> 254 <ul 255 v-else-if="communityContributors.length" 256 class="grid grid-cols-[repeat(auto-fill,48px)] justify-center gap-2 list-none p-0" 257 > 258 <li 259 v-for="contributor in communityContributors" 260 :key="contributor.id" 261 class="group relative" 262 > 263 <LinkBase 264 :to="contributor.html_url" 265 no-underline 266 no-external-icon 267 :aria-label="$t('about.contributors.view_profile', { name: contributor.login })" 268 > 269 <img 270 :src="`${contributor.avatar_url}&s=64`" 271 :alt="`${contributor.login}'s avatar`" 272 width="48" 273 height="48" 274 class="w-12 h-12 rounded-lg ring-2 ring-transparent group-hover:ring-accent transition-all duration-200 ease-out hover:scale-125 will-change-transform" 275 loading="lazy" 276 /> 277 <span 278 class="pointer-events-none absolute -top-9 inset-is-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs px-2 py-1 shadow-lg opacity-0 scale-95 transition-all duration-150 group-hover:opacity-100 group-hover:scale-100" 279 dir="ltr" 280 role="tooltip" 281 > 282 @{{ contributor.login }} 283 </span> 284 </LinkBase> 285 </li> 286 </ul> 287 </section> 288 </div> 289 290 <CallToAction /> 291 </section> 292 </article> 293 </main> 294</template>