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