Skip to content

Commit 805b335

Browse files
authored
Merge branch 'main' into feat/skills-modal-tabs
2 parents 9db7a51 + 192c398 commit 805b335

49 files changed

Lines changed: 859 additions & 160 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/autofix.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ jobs:
3434
- name: 📦 Install browsers
3535
run: pnpm playwright install
3636

37+
- name: 🌐 Compare translations
38+
run: pnpm i18n:check
39+
3740
- name: 🌍 Update lunaria data
3841
run: pnpm build:lunaria
3942

CONTRIBUTING.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,23 @@ To add a new locale:
284284

285285
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization) for more info.
286286

287+
### Update translation
288+
289+
We track the current progress of translations with [Lunaria](https://lunaria.dev/) on this site: https://i18n.npmx.dev/
290+
If you see any outdated translations in your language, feel free to update the keys to match then English version.
291+
292+
In order to make sure you have everything up-to-date, you can run:
293+
294+
```bash
295+
pnpm i18n:check <country-code>
296+
```
297+
298+
For example to check if all Japanese translation keys are up-to-date, run:
299+
300+
```bash
301+
pnpm i18n:check ja-JP
302+
```
303+
287304
#### Country variants (advanced)
288305

289306
Most languages only need a single locale file. Country variants are only needed when you want to support regional differences (e.g., `es-ES` for Spain vs `es-419` for Latin America).

app/components/AuthModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ async function handleLogin() {
5151
<button
5252
type="button"
5353
class="absolute inset-0 bg-black/60 cursor-default"
54-
:aria-label="$t('auth.modal.close')"
54+
:aria-label="$t('common.close')"
5555
@click="open = false"
5656
/>
5757

app/components/ClaimPackageModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ const connectorModalOpen = shallowRef(false)
141141
<button
142142
type="button"
143143
class="absolute inset-0 bg-black/60 cursor-default"
144-
:aria-label="$t('claim.modal.close_modal')"
144+
:aria-label="$t('common.close_modal')"
145145
@click="open = false"
146146
/>
147147

app/components/ColumnPicker.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function handleReset() {
9393
v-if="isOpen"
9494
ref="menuRef"
9595
:id="menuId"
96-
class="absolute inset-ie-0 mt-2 w-60 bg-bg-subtle border border-border rounded-lg shadow-lg z-20"
96+
class="absolute inset-is-0 sm:inset-is-auto sm:inset-ie-0 mt-2 w-60 bg-bg-subtle border border-border rounded-lg shadow-lg z-20"
9797
role="group"
9898
:aria-label="$t('filters.columns.show')"
9999
>

app/components/ConnectorModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ watch(open, isOpen => {
5959
<button
6060
type="button"
6161
class="absolute inset-0 bg-black/60 cursor-default"
62-
:aria-label="$t('connector.modal.close_modal')"
62+
:aria-label="$t('common.close_modal')"
6363
@click="open = false"
6464
/>
6565

app/components/HeaderAccountMenu.client.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,11 @@ function openAuthModal() {
165165
: 'bg-blue-500/20 text-blue-500'
166166
"
167167
>
168-
{{ operationCount }} {{ $t('account_menu.ops') }}
168+
{{
169+
$t('account_menu.ops', {
170+
count: operationCount,
171+
})
172+
}}
169173
</span>
170174
</button>
171175

app/components/MarkdownText.vue

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,45 @@ const props = defineProps<{
33
text: string
44
/** When true, renders link text without the anchor tag (useful when inside another link) */
55
plain?: boolean
6+
/** Package name to strip from the beginning of the description (if present) */
7+
packageName?: string
68
}>()
79
8-
// Escape HTML to prevent XSS
9-
function escapeHtml(text: string): string {
10-
return text
10+
// Strip markdown image badges from text
11+
function stripMarkdownImages(text: string): string {
12+
// Remove linked images: [![alt](image-url)](link-url) - handles incomplete URLs too
13+
// Using {0,500} instead of * to prevent ReDoS on pathological inputs
14+
text = text.replace(/\[!\[[^\]]{0,500}\]\([^)]{0,2000}\)\]\([^)]{0,2000}\)?/g, '')
15+
// Remove standalone images: ![alt](url)
16+
text = text.replace(/!\[[^\]]{0,500}\]\([^)]{0,2000}\)/g, '')
17+
// Remove any leftover empty links or broken markdown link syntax
18+
text = text.replace(/\[\]\([^)]{0,2000}\)?/g, '')
19+
return text.trim()
20+
}
21+
22+
// Strip HTML tags and escape remaining HTML to prevent XSS
23+
function stripAndEscapeHtml(text: string): string {
24+
// First strip markdown image badges
25+
let stripped = stripMarkdownImages(text)
26+
27+
// Then strip actual HTML tags (keep their text content)
28+
// Only match tags that start with a letter or / (to avoid matching things like "a < b > c")
29+
stripped = stripped.replace(/<\/?[a-z][^>]*>/gi, '')
30+
31+
if (props.packageName) {
32+
// Trim first to handle leading/trailing whitespace from stripped HTML
33+
stripped = stripped.trim()
34+
// Collapse multiple whitespace into single space
35+
stripped = stripped.replace(/\s+/g, ' ')
36+
// Escape special regex characters in package name
37+
const escapedName = props.packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
38+
// Match package name at the start, optionally followed by: space, dash, colon, hyphen, or just space
39+
const namePattern = new RegExp(`^${escapedName}\\s*[-:—]?\\s*`, 'i')
40+
stripped = stripped.replace(namePattern, '').trim()
41+
}
42+
43+
// Then escape any remaining HTML entities
44+
return stripped
1145
.replace(/&/g, '&amp;')
1246
.replace(/</g, '&lt;')
1347
.replace(/>/g, '&gt;')
@@ -19,8 +53,8 @@ function escapeHtml(text: string): string {
1953
function parseMarkdown(text: string): string {
2054
if (!text) return ''
2155
22-
// First escape HTML
23-
let html = escapeHtml(text)
56+
// First strip HTML tags and escape remaining HTML
57+
let html = stripAndEscapeHtml(text)
2458
2559
// Bold: **text** or __text__
2660
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')

app/components/OgImage/Default.vue

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,81 @@
1+
<script setup lang="ts">
2+
interface Props {
3+
primaryColor?: string
4+
title?: string
5+
description?: string
6+
}
7+
8+
const props = withDefaults(defineProps<Props>(), {
9+
primaryColor: '#60a5fa',
10+
title: 'npmx',
11+
description: 'A better browser for the **npm registry**',
12+
})
13+
</script>
14+
115
<template>
2-
<div class="h-full w-full flex flex-col justify-center items-center bg-[#0a0a0a] text-[#fafafa]">
3-
<h1 class="text-6xl font-medium tracking-tight" style="font-family: 'Geist Mono'">
4-
<span class="text-fg-subtle" style="letter-spacing: -0.1em">./</span> npmx
5-
</h1>
6-
<h1 class="text-3xl font-medium tracking-tight">a better browser for the npm registry</h1>
16+
<div
17+
class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden"
18+
>
19+
<div class="relative z-10 flex flex-col gap-6">
20+
<div class="flex items-start gap-4">
21+
<div
22+
class="flex items-start justify-center w-16 h-16 rounded-xl bg-gradient-to-tr from-[#3b82f6] shadow-lg"
23+
:style="{ backgroundColor: props.primaryColor }"
24+
>
25+
<svg
26+
width="36"
27+
height="36"
28+
viewBox="0 0 24 24"
29+
fill="none"
30+
stroke="white"
31+
stroke-width="2.5"
32+
stroke-linecap="round"
33+
stroke-linejoin="round"
34+
>
35+
<path d="m7.5 4.27 9 5.15" />
36+
<path
37+
d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"
38+
/>
39+
<path d="m3.3 7 8.7 5 8.7-5" />
40+
<path d="M12 22V12" />
41+
</svg>
42+
</div>
43+
44+
<h1
45+
class="text-8xl font-bold tracking-tighter"
46+
style="font-family: 'Geist Sans', sans-serif"
47+
>
48+
<span class="opacity-80" :style="{ color: props.primaryColor }">./</span>{{ props.title }}
49+
</h1>
50+
</div>
51+
52+
<div
53+
class="flex flex-wrap items-center gap-x-3 text-4xl font-light text-[#a3a3a3]"
54+
style="font-family: 'Geist Sans', sans-serif"
55+
>
56+
<template v-for="(part, index) in props.description.split(/(\*\*.*?\*\*)/)" :key="index">
57+
<span
58+
v-if="part.startsWith('**') && part.endsWith('**')"
59+
class="px-3 py-1 rounded-lg border font-normal"
60+
:style="{
61+
color: props.primaryColor,
62+
backgroundColor: props.primaryColor + '10',
63+
borderColor: props.primaryColor + '30',
64+
boxShadow: `0 0 20px ${props.primaryColor}25`,
65+
}"
66+
>
67+
{{ part.replaceAll('**', '') }}
68+
</span>
69+
<span v-else-if="part.trim() !== ''">
70+
{{ part }}
71+
</span>
72+
</template>
73+
</div>
74+
</div>
775

8-
<p class="absolute bottom-12 text-lg text-[#404040]">npmx.dev</p>
76+
<div
77+
class="absolute -top-32 -right-32 w-[550px] h-[550px] rounded-full blur-3xl"
78+
:style="{ backgroundColor: props.primaryColor + '10' }"
79+
/>
980
</div>
1081
</template>

app/components/OgImage/Package.vue

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,95 @@ withDefaults(
55
version: string
66
downloads?: string
77
license?: string
8+
primaryColor?: string
89
}>(),
910
{
1011
downloads: '',
1112
license: '',
13+
primaryColor: '#60a5fa',
1214
},
1315
)
1416
</script>
1517

1618
<template>
1719
<div
18-
class="h-full w-full flex flex-col justify-center items-center bg-[#0a0a0a] text-[#fafafa]"
19-
style="font-family: 'Geist Mono'"
20+
class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden"
2021
>
21-
<h1 class="text-6xl font-medium tracking-tight">
22-
{{ name }}
23-
</h1>
22+
<div class="relative z-10 flex flex-col gap-6">
23+
<div class="flex items-start gap-4">
24+
<div
25+
class="flex items-start justify-center w-16 h-16 rounded-xl shadow-lg bg-gradient-to-tr from-[#3b82f6]"
26+
:style="{ backgroundColor: primaryColor }"
27+
>
28+
<svg
29+
width="36"
30+
height="36"
31+
viewBox="0 0 24 24"
32+
fill="none"
33+
stroke="white"
34+
stroke-width="2.5"
35+
stroke-linecap="round"
36+
stroke-linejoin="round"
37+
>
38+
<path d="m7.5 4.27 9 5.15" />
39+
<path
40+
d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"
41+
/>
42+
<path d="m3.3 7 8.7 5 8.7-5" />
43+
<path d="M12 22V12" />
44+
</svg>
45+
</div>
2446

25-
<div class="flex items-center gap-6 mt-6 text-xl text-fg-subtle">
26-
<span>v{{ version }}</span>
27-
<span v-if="downloads">{{ downloads }}/wk</span>
28-
<span v-if="license">{{ license }}</span>
47+
<h1
48+
class="text-8xl font-bold tracking-tighter"
49+
style="font-family: 'Geist Sans', sans-serif"
50+
>
51+
<span :style="{ color: primaryColor }" class="opacity-80">./</span>{{ name }}
52+
</h1>
53+
</div>
54+
55+
<div
56+
class="flex items-center gap-3 text-4xl font-light text-[#a3a3a3]"
57+
style="font-family: 'Geist Sans', sans-serif"
58+
>
59+
<span
60+
class="px-3 py-1 rounded-lg border"
61+
:style="{
62+
color: primaryColor,
63+
backgroundColor: primaryColor + '10',
64+
borderColor: primaryColor + '30',
65+
boxShadow: `0 0 20px ${primaryColor}25`,
66+
}"
67+
>
68+
{{ version }}
69+
</span>
70+
<span v-if="downloads">
71+
<span>• {{ downloads }} </span>
72+
<span class="flex items-center gap-0">
73+
<svg
74+
width="30"
75+
height="30"
76+
viewBox="0 0 24 24"
77+
fill="none"
78+
stroke="currentColor"
79+
stroke-width="2"
80+
stroke-linecap="round"
81+
stroke-linejoin="round"
82+
class="text-white/70"
83+
>
84+
<circle cx="12" cy="12" r="10" class="opacity-40" />
85+
<path d="M12 8v8m-3-3l3 3 3-3" />
86+
</svg>
87+
<span>/wk</span>
88+
</span>
89+
</span>
90+
<span v-if="license"> • {{ license }}</span>
91+
</div>
2992
</div>
3093

31-
<p class="absolute bottom-12 text-lg text-[#404040]">npmx.dev</p>
94+
<div
95+
class="absolute -top-32 -right-32 w-[550px] h-[550px] rounded-full blur-3xl"
96+
:style="{ backgroundColor: primaryColor + '10' }"
97+
/>
3298
</div>
3399
</template>

0 commit comments

Comments
 (0)