Skip to content

Commit 215037f

Browse files
authored
Merge branch 'main' into accessible-accent
2 parents ed80442 + 6d0b51b commit 215037f

17 files changed

Lines changed: 328 additions & 36 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
#secure password, can use openssl rand --hex 32
1+
#secure password, can use openssl rand -hex 32
22
NUXT_SESSION_PASSWORD=""

app/assets/main.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ button:focus-visible {
205205
background: #404040;
206206
}
207207

208+
/* Scrollbar styling for Firefox */
209+
@supports not selector(::-webkit-scrollbar) {
210+
* {
211+
scrollbar-width: thin;
212+
scrollbar-color: var(--border) var(--bg);
213+
}
214+
}
215+
208216
/* Shiki theme colors */
209217
html.light .shiki,
210218
html.light .shiki span {

app/components/User/Avatar.vue

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
username: string
4+
}>()
5+
6+
const { data: gravatarUrl } = useLazyFetch(() => `/api/gravatar/${props.username}`, {
7+
transform: res => (res.hash ? `/_avatar/${res.hash}?s=128&d=404` : null),
8+
getCachedData(key, nuxtApp) {
9+
return nuxtApp.static.data[key] ?? nuxtApp.payload.data[key]
10+
},
11+
})
12+
</script>
13+
14+
<template>
15+
<!-- Avatar -->
16+
<div
17+
class="size-16 shrink-0 rounded-full bg-bg-muted border border-border flex items-center justify-center overflow-hidden"
18+
role="img"
19+
:aria-label="`Avatar for ${username}`"
20+
>
21+
<!-- If Gravatar was fetched, display it -->
22+
<img
23+
v-if="gravatarUrl"
24+
:src="gravatarUrl"
25+
alt=""
26+
width="64"
27+
height="64"
28+
class="w-full h-full object-cover"
29+
/>
30+
<!-- Else fallback to initials -->
31+
<span v-else class="text-2xl text-fg-subtle font-mono" aria-hidden="true">
32+
{{ username.charAt(0).toUpperCase() }}
33+
</span>
34+
</div>
35+
</template>

app/components/compare/FacetRow.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function isCellLoading(index: number): boolean {
7878
<div
7979
v-for="(value, index) in values"
8080
:key="index"
81-
class="comparison-cell relative flex items-end justify-center px-4 py-3 border-b border-border"
81+
class="comparison-cell relative flex items-center justify-center px-4 py-3 border-b border-border"
8282
>
8383
<!-- Background bar for numeric values -->
8484
<div
@@ -103,7 +103,10 @@ function isCellLoading(index: number): boolean {
103103

104104
<!-- Value display -->
105105
<template v-else>
106-
<span class="relative font-mono text-sm tabular-nums" :class="getStatusClass(value.status)">
106+
<span
107+
class="relative font-mono text-sm text-center tabular-nums"
108+
:class="getStatusClass(value.status)"
109+
>
107110
<!-- Date values use DateTime component for i18n and user settings -->
108111
<DateTime v-if="value.type === 'date'" :datetime="value.display" date-style="medium" />
109112
<template v-else>{{ value.display }}</template>

app/pages/~[username]/index.vue

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -178,15 +178,7 @@ defineOgImageComponent('Default', {
178178
<!-- Header -->
179179
<header class="mb-8 pb-8 border-b border-border">
180180
<div class="flex flex-wrap items-center gap-4">
181-
<!-- Avatar placeholder -->
182-
<div
183-
class="size-16 shrink-0 rounded-full bg-bg-muted border border-border flex items-center justify-center"
184-
aria-hidden="true"
185-
>
186-
<span class="text-2xl text-fg-subtle font-mono">{{
187-
username.charAt(0).toUpperCase()
188-
}}</span>
189-
</div>
181+
<UserAvatar :username="username" />
190182
<div>
191183
<h1 class="font-mono text-2xl sm:text-3xl font-medium">~{{ username }}</h1>
192184
<p v-if="results?.total" class="text-fg-muted text-sm mt-1">

app/pages/~[username]/orgs.vue

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,7 @@ defineOgImageComponent('Default', {
120120
<!-- Header -->
121121
<header class="mb-8 pb-8 border-b border-border">
122122
<div class="flex flex-wrap items-center gap-4 mb-4">
123-
<!-- Avatar placeholder -->
124-
<div
125-
class="size-16 shrink-0 rounded-full bg-bg-muted border border-border flex items-center justify-center"
126-
aria-hidden="true"
127-
>
128-
<span class="text-2xl text-fg-subtle font-mono">{{
129-
username.charAt(0).toUpperCase()
130-
}}</span>
131-
</div>
123+
<UserAvatar :username="username" />
132124
<div>
133125
<h1 class="font-mono text-2xl sm:text-3xl font-medium">~{{ username }}</h1>
134126
<p class="text-fg-muted text-sm mt-1">{{ $t('user.orgs_page.title') }}</p>

i18n/locales/pt-BR.json

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"found_packages": "Nenhum pacote encontrado | 1 pacote encontrado | {count} pacotes encontrados",
2828
"updating": "(atualizando...)",
2929
"no_results": "Nenhum pacote encontrado para \"{query}\"",
30+
"title": "pesquisar",
3031
"not_taken": "{name} não está em uso",
3132
"claim_prompt": "Reivindicar este nome de pacote no npm",
3233
"claim_button": "Reivindicar \"{name}\"",
@@ -142,6 +143,7 @@
142143
"install_size": "Tamanho de Instalação",
143144
"vulns": "Vulnerabilidades",
144145
"updated": "Atualizado",
146+
"skills": "Habilidades",
145147
"view_dependency_graph": "Ver gráfico de dependências",
146148
"inspect_dependency_tree": "Inspecionar árvore de dependências",
147149
"size_tooltip": {
@@ -150,7 +152,22 @@
150152
}
151153
},
152154
"skills": {
153-
"file_counts": {}
155+
"title": "Habilidades do Agente",
156+
"skills_available": "{count} habilidade disponível | {count} habilidades disponíveis",
157+
"view": "Ver",
158+
"compatible_with": "Compatível com {tool}",
159+
"install": "Instalar",
160+
"installation_method": "Método de Instalação",
161+
"learn_more": "Saiba mais",
162+
"available_skills": "Habilidades Disponíveis",
163+
"click_to_expand": "Clique para expandir",
164+
"no_description": "Sem descrição",
165+
"file_counts": {
166+
"scripts": "{count} script | {count} scripts",
167+
"refs": "{count} ref | {count} refs",
168+
"assets": "{count} asset | {count} assets"
169+
},
170+
"view_source": "Ver código-fonte"
154171
},
155172
"links": {
156173
"repo": "repositório",
@@ -281,7 +298,8 @@
281298
"no_types": "Sem tipos TypeScript"
282299
},
283300
"license": {
284-
"view_spdx": "Ver texto da licença no SPDX"
301+
"view_spdx": "Ver texto da licença no SPDX",
302+
"none": "Nenhuma"
285303
},
286304
"vulnerabilities": {
287305
"no_description": "Nenhuma descrição disponível",
@@ -754,27 +772,27 @@
754772
"connect": "conectar",
755773
"account": "Conta",
756774
"npm_cli": "npm CLI",
757-
"atmosphere": "Atmosphere",
775+
"atmosphere": "Atmosfera",
758776
"npm_cli_desc": "Gerenciar pacotes e organizações",
759777
"atmosphere_desc": "Recursos sociais e identidade",
760778
"connect_npm_cli": "Conectar ao CLI npm",
761-
"connect_atmosphere": "Conectar ao Atmosphere",
779+
"connect_atmosphere": "Conectar à Atmosfera",
762780
"connecting": "Conectando...",
763781
"ops": "{count} op | {count} ops",
764782
"disconnect": "Desconectar"
765783
},
766784
"auth": {
767785
"modal": {
768-
"title": "Atmosphere",
786+
"title": "Atmosfera",
769787
"connected_as": "Conectado como {'@'}{handle}",
770788
"disconnect": "Desconectar",
771-
"connect_prompt": "Conecte-se com sua conta Atmosphere",
789+
"connect_prompt": "Conecte-se com sua conta da Atmosfera",
772790
"handle_label": "Nome de identificação",
773791
"handle_placeholder": "alice.npmx.social",
774792
"connect": "Conectar",
775793
"create_account": "Criar uma nova conta",
776794
"connect_bluesky": "Conectar com Bluesky",
777-
"what_is_atmosphere": "O que é uma conta Atmosphere?",
795+
"what_is_atmosphere": "O que é uma conta da Atmosfera?",
778796
"atmosphere_explanation": "{npmx} usa o {atproto} para alimentar muitos de seus recursos sociais, permitindo que os usuários possuam seus dados e usem uma conta para todos os aplicativos compatíveis. Depois de criar uma conta, você pode usar outros aplicativos como {bluesky} e {tangled} com a mesma conta."
779797
}
780798
},

lunaria/files/pt-BR.json

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"found_packages": "Nenhum pacote encontrado | 1 pacote encontrado | {count} pacotes encontrados",
2828
"updating": "(atualizando...)",
2929
"no_results": "Nenhum pacote encontrado para \"{query}\"",
30+
"title": "pesquisar",
3031
"not_taken": "{name} não está em uso",
3132
"claim_prompt": "Reivindicar este nome de pacote no npm",
3233
"claim_button": "Reivindicar \"{name}\"",
@@ -142,6 +143,7 @@
142143
"install_size": "Tamanho de Instalação",
143144
"vulns": "Vulnerabilidades",
144145
"updated": "Atualizado",
146+
"skills": "Habilidades",
145147
"view_dependency_graph": "Ver gráfico de dependências",
146148
"inspect_dependency_tree": "Inspecionar árvore de dependências",
147149
"size_tooltip": {
@@ -150,7 +152,22 @@
150152
}
151153
},
152154
"skills": {
153-
"file_counts": {}
155+
"title": "Habilidades do Agente",
156+
"skills_available": "{count} habilidade disponível | {count} habilidades disponíveis",
157+
"view": "Ver",
158+
"compatible_with": "Compatível com {tool}",
159+
"install": "Instalar",
160+
"installation_method": "Método de Instalação",
161+
"learn_more": "Saiba mais",
162+
"available_skills": "Habilidades Disponíveis",
163+
"click_to_expand": "Clique para expandir",
164+
"no_description": "Sem descrição",
165+
"file_counts": {
166+
"scripts": "{count} script | {count} scripts",
167+
"refs": "{count} ref | {count} refs",
168+
"assets": "{count} asset | {count} assets"
169+
},
170+
"view_source": "Ver código-fonte"
154171
},
155172
"links": {
156173
"repo": "repositório",
@@ -281,7 +298,8 @@
281298
"no_types": "Sem tipos TypeScript"
282299
},
283300
"license": {
284-
"view_spdx": "Ver texto da licença no SPDX"
301+
"view_spdx": "Ver texto da licença no SPDX",
302+
"none": "Nenhuma"
285303
},
286304
"vulnerabilities": {
287305
"no_description": "Nenhuma descrição disponível",
@@ -754,27 +772,27 @@
754772
"connect": "conectar",
755773
"account": "Conta",
756774
"npm_cli": "npm CLI",
757-
"atmosphere": "Atmosphere",
775+
"atmosphere": "Atmosfera",
758776
"npm_cli_desc": "Gerenciar pacotes e organizações",
759777
"atmosphere_desc": "Recursos sociais e identidade",
760778
"connect_npm_cli": "Conectar ao CLI npm",
761-
"connect_atmosphere": "Conectar ao Atmosphere",
779+
"connect_atmosphere": "Conectar à Atmosfera",
762780
"connecting": "Conectando...",
763781
"ops": "{count} op | {count} ops",
764782
"disconnect": "Desconectar"
765783
},
766784
"auth": {
767785
"modal": {
768-
"title": "Atmosphere",
786+
"title": "Atmosfera",
769787
"connected_as": "Conectado como {'@'}{handle}",
770788
"disconnect": "Desconectar",
771-
"connect_prompt": "Conecte-se com sua conta Atmosphere",
789+
"connect_prompt": "Conecte-se com sua conta da Atmosfera",
772790
"handle_label": "Nome de identificação",
773791
"handle_placeholder": "alice.npmx.social",
774792
"connect": "Conectar",
775793
"create_account": "Criar uma nova conta",
776794
"connect_bluesky": "Conectar com Bluesky",
777-
"what_is_atmosphere": "O que é uma conta Atmosphere?",
795+
"what_is_atmosphere": "O que é uma conta da Atmosfera?",
778796
"atmosphere_explanation": "{npmx} usa o {atproto} para alimentar muitos de seus recursos sociais, permitindo que os usuários possuam seus dados e usem uma conta para todos os aplicativos compatíveis. Depois de criar uma conta, você pode usar outros aplicativos como {bluesky} e {tangled} com a mesma conta."
779797
}
780798
},

nuxt.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ export default defineNuxtConfig({
105105
'/api/registry/docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
106106
'/api/registry/file/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
107107
'/api/registry/files/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
108+
'/_avatar/**': {
109+
isr: 3600,
110+
proxy: {
111+
to: 'https://www.gravatar.com/avatar/**',
112+
},
113+
},
108114
// static pages
109115
'/about': { prerender: true },
110116
'/settings': { prerender: true },
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createError } from 'h3'
2+
import * as v from 'valibot'
3+
import { GravatarQuerySchema } from '#shared/schemas/user'
4+
import { getGravatarFromUsername } from '#server/utils/gravatar'
5+
import { handleApiError } from '#server/utils/error-handler'
6+
7+
export default defineCachedEventHandler(
8+
async event => {
9+
const rawUsername = getRouterParam(event, 'username')
10+
11+
try {
12+
const { username } = v.parse(GravatarQuerySchema, {
13+
username: rawUsername,
14+
})
15+
16+
const hash = await getGravatarFromUsername(username)
17+
18+
if (!hash) {
19+
throw createError({
20+
statusCode: 404,
21+
message: ERROR_GRAVATAR_EMAIL_UNAVAILABLE,
22+
})
23+
}
24+
25+
return { hash }
26+
} catch (error: unknown) {
27+
handleApiError(error, {
28+
statusCode: 502,
29+
message: ERROR_GRAVATAR_FETCH_FAILED,
30+
})
31+
}
32+
},
33+
{
34+
maxAge: CACHE_MAX_AGE_ONE_DAY,
35+
swr: true,
36+
getKey: event => {
37+
const username = getRouterParam(event, 'username')?.trim().toLowerCase()
38+
return `gravatar:v1:${username}`
39+
},
40+
},
41+
)

0 commit comments

Comments
 (0)