Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
02ad831
fix(i18n): add missing pt-BR translations for "shortcuts" (#2532)
tcelestino Apr 15, 2026
0ca1692
fix: set default smoothing to 0 (#2535)
graphieros Apr 15, 2026
5fc9e99
fix(i18n): format compare sparkline data labels (#2537)
graphieros Apr 15, 2026
609a391
chore(i18n): fix lunaria pre-commit hook with force: true (#2528)
romansp Apr 15, 2026
32bf8bb
feat: add timeline tab to package page (#2245)
43081j Apr 15, 2026
75bce20
chore: add issue types and missing label to issue templates (#2544)
mootari Apr 15, 2026
c441377
fix(i18n): add missing translations to Brazilian Portuguese (#2542)
tcelestino Apr 16, 2026
6db03a6
feat(i18n): update French translations (#2547)
Limerio Apr 16, 2026
ca5e399
fix: use fast-npm-meta in timeline (#2546)
43081j Apr 16, 2026
b541ee2
docs(ui): add stories for Settings page (#2545)
cylewaitforit Apr 16, 2026
da0897b
fix: show integers for values below 1k in trends chart tooltip (#2550)
graphieros Apr 16, 2026
791ce70
fix: resolve readme copy functionality in Safari (#2554)
MatteoGabriele Apr 17, 2026
b9c3a1d
feat: new og images (#2292)
harlan-zw Apr 17, 2026
cbcdc54
fix: avoid showing "No README is available" during README load (#2473)
akadotsh Apr 17, 2026
b7f71a6
fix: round chart numbers in `applyDataCorrection` for consistent roun…
ulrichstark Apr 18, 2026
ca5c9b4
fix(i18n): update French localizations (#2560)
WarningImHack3r Apr 18, 2026
d9f8d56
feat: add stale workflow (#2555)
MatteoGabriele Apr 18, 2026
e365dd4
chore: increase operation per run in stale workflow (#2564)
MatteoGabriele Apr 18, 2026
5f30d25
chore: remove stale bot (#2566)
MatteoGabriele Apr 18, 2026
400ddf8
feat(i18n): update hi-IN and mr-IN translation for npmx tagline (#2567)
trivikr Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#secure password, can use openssl rand -hex 32
NUXT_SESSION_PASSWORD=""

#HMAC secret for image proxy URL signing, can use openssl rand -hex 32
#HMAC secret for image-proxy and OG image URL signing, can use openssl rand -hex 32
NUXT_IMAGE_PROXY_SECRET=""
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve npmx
type: bug
labels: ['pending triage']
body:
- type: markdown
attributes:
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature-request.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: '🚀 Feature request'
description: Suggest a feature that will improve npmx
type: feature
labels: ['pending triage']
body:
- type: markdown
Expand Down
65 changes: 65 additions & 0 deletions .storybook/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,68 @@ export const pdsUsersHandler = http.get('/api/atproto/pds-users', () => {
},
])
})

export const i18nStatusHandler = http.get('/lunaria/status.json', () => {
return HttpResponse.json({
generatedAt: '2026-01-22T10:07:07.000Z',
sourceLocale: {
lang: 'en',
label: 'English',
totalKeys: 500,
},
locales: [
{
lang: 'en-GB',
label: 'English (UK)',
dir: 'ltr',
totalKeys: 500,
completedKeys: 423,
percentComplete: 84,
missingKeys: [
'settings.background_themes.label',
'settings.enable_graph_pulse_loop',
'settings.enable_graph_pulse_loop_description',
'settings.data_source.algolia_description',
'settings.data_source.npm_description',
'i18n.contribute_hint',
'i18n.copy_keys',
],
githubEditUrl: 'https://github.com/npmx-dev/npmx.dev/edit/main/i18n/locales/en-GB.json',
githubHistoryUrl:
'https://github.com/npmx-dev/npmx.dev/commits/main/i18n/locales/en-GB.json',
},
{
lang: 'fr-FR',
label: 'Français',
dir: 'ltr',
totalKeys: 500,
completedKeys: 423,
percentComplete: 84,
missingKeys: [
'settings.background_themes.label',
'settings.enable_graph_pulse_loop',
'settings.enable_graph_pulse_loop_description',
'settings.data_source.algolia_description',
'settings.data_source.npm_description',
'i18n.contribute_hint',
'i18n.copy_keys',
],
githubEditUrl: 'https://github.com/npmx-dev/npmx.dev/edit/main/i18n/locales/fr-FR.json',
githubHistoryUrl:
'https://github.com/npmx-dev/npmx.dev/commits/main/i18n/locales/fr-FR.json',
},
{
lang: 'de-DE',
label: 'Deutsch',
dir: 'ltr',
totalKeys: 500,
completedKeys: 500,
percentComplete: 100,
missingKeys: [],
githubEditUrl: 'https://github.com/npmx-dev/npmx.dev/edit/main/i18n/locales/de-DE.json',
githubHistoryUrl:
'https://github.com/npmx-dev/npmx.dev/commits/main/i18n/locales/de-DE.json',
},
],
})
})
16 changes: 16 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,19 @@
background-color: var(--bg, oklch(0.171 0 0)) !important;
}
</style>
<script>
// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
// Stub Nuxt specific globals
// @nuxtjs/color-mode's plugin.client.js reads window[globalName] at module
// evaluation time — before any Storybook setup() callback runs — so the
// global must exist in the HTML head, not in preview.ts.
window.__NUXT_COLOR_MODE__ ??= {
preference: 'system',
value: 'dark',
getColorScheme: function () {
return 'dark'
},
addColorScheme: function () {},
removeColorScheme: function () {},
}
</script>
12 changes: 1 addition & 11 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,8 @@ import npmxDark from './theme'

initialize()

// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
// Stub Nuxt specific globals
// @ts-expect-error - dynamic global name
globalThis['__NUXT_COLOR_MODE__'] ??= {
preference: 'system',
value: 'dark',
getColorScheme: fn(() => 'dark'),
addColorScheme: fn(),
removeColorScheme: fn(),
}
// @ts-expect-error - dynamic global name
globalThis.defineOgImageComponent = fn()
globalThis.defineOgImage = fn()

// Subscribe to locale changes from storybook-i18n addon (once, outside decorator)
let currentI18nInstance: any = null
Expand Down
4 changes: 4 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ if (import.meta.client) {
useEventListener(document, 'click', handleModalLightDismiss)
}
}
// title and description will be inferred
// this will be overridden by upstream pages that use different templates
defineOgImage('Page.takumi', {}, { alt: 'npmx — a fast, modern browser for the npm registry' })
</script>

<template>
Expand Down
4 changes: 4 additions & 0 deletions app/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ const closeModal = () => modalRef.value?.close?.()
<kbd class="kbd">f</kbd>
<span>{{ $t('shortcuts.open_diff') }}</span>
</li>
<li class="flex gap-2 items-center">
<kbd class="kbd">t</kbd>
<span>{{ $t('shortcuts.open_timeline') }}</span>
</li>
<li class="flex gap-2 items-center">
<kbd class="kbd">c</kbd>
<span>{{ $t('shortcuts.compare_from_package') }}</span>
Expand Down
4 changes: 4 additions & 0 deletions app/components/Chart/SplitSparkline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const props = defineProps<{
const { locale } = useI18n()
const colorMode = useColorMode()
const numberFormatter = useNumberFormatter()
const resolvedMode = shallowRef<'light' | 'dark'>('light')
const rootEl = shallowRef<HTMLElement | null>(null)
const palette = getPalette('')
Expand Down Expand Up @@ -153,6 +154,9 @@ const configs = computed(() => {
fontSize: 24,
bold: false,
color: colors.value.fg,
formatter: ({ value }) => {
return numberFormatter.value.format(value)
},
datetimeFormatter: {
enable: true,
locale: locale.value,
Expand Down
22 changes: 22 additions & 0 deletions app/components/OgBrand.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
height?: number
}>(),
{
height: 60,
},
)

const width = computed(() => Math.round(props.height * (602 / 170)))
</script>

<template>
<img
src="/logo.svg"
alt="npmx"
:width="width"
:height="height"
:style="{ width: `${width}px`, height: `${height}px` }"
/>
</template>
13 changes: 0 additions & 13 deletions app/components/OgImage/BlogPost.d.vue.ts

This file was deleted.

115 changes: 115 additions & 0 deletions app/components/OgImage/BlogPost.takumi.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script setup lang="ts">
import type { ResolvedAuthor } from '#shared/schemas/blog'

const {
title,
authors = [],
date = '',
} = defineProps<{
title: string
authors?: ResolvedAuthor[]
date?: string
}>()

const formattedDate = computed(() => {
if (!date) return ''
const parsed = new Date(date)
if (Number.isNaN(parsed.getTime())) return date

return parsed.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
})
})

const MAX_VISIBLE_AUTHORS = 2

const getInitials = (name: string) =>
name
.trim()
.split(/\s+/)
.map(part => part[0] ?? '')
.join('')
.toUpperCase()
.slice(0, 2)

const visibleAuthors = computed(() => {
if (authors.length <= 3) return authors
return authors.slice(0, MAX_VISIBLE_AUTHORS)
})

const extraCount = computed(() => {
if (authors.length <= 3) return 0
return authors.length - MAX_VISIBLE_AUTHORS
})

const formattedAuthorNames = computed(() => {
const allNames = authors.map(a => a.name)
if (allNames.length === 0) return ''
if (allNames.length === 1) return allNames[0]
if (allNames.length === 2) return `${allNames[0]} and ${allNames[1]}`
if (allNames.length === 3) return `${allNames[0]}, ${allNames[1]}, and ${allNames[2]}`
const shown = allNames.slice(0, MAX_VISIBLE_AUTHORS)
const remaining = allNames.length - MAX_VISIBLE_AUTHORS
return `${shown.join(', ')} and ${remaining} others`
})
</script>

<template>
<OgLayout>
<div class="px-15 py-12 flex flex-col justify-center gap-5 h-full">
<OgBrand :height="48" />

<!-- Date + Title -->
<div class="flex flex-col gap-2">
<span v-if="formattedDate" class="text-3xl text-fg-muted">
{{ formattedDate }}
</span>

<div
class="lg:text-6xl text-5xl tracking-tighter font-mono leading-tight"
:style="{ lineClamp: 2, textOverflow: 'ellipsis' }"
>
{{ title }}
</div>
</div>

<!-- Authors -->
<div v-if="authors.length" class="flex items-center gap-4 flex-nowrap">
<!-- Stacked avatars -->
<span class="flex flex-row items-center">
<span
v-for="(author, index) in visibleAuthors"
:key="author.name"
class="flex items-center justify-center rounded-full border border-bg bg-bg-muted overflow-hidden w-12 h-12"
:style="{ marginLeft: index > 0 ? '-20px' : '0' }"
>
<img
v-if="author.avatar"
:src="author.avatar"
:alt="author.name"
width="48"
height="48"
class="w-full h-full object-cover"
/>
<span v-else class="text-5 text-fg-muted font-medium">
{{ getInitials(author.name) }}
</span>
</span>
<!-- +N badge -->
<span
v-if="extraCount > 0"
class="flex items-center justify-center text-lg font-medium text-fg-muted rounded-full border border-bg bg-bg-muted overflow-hidden w-12 h-12"
:style="{ marginLeft: '-20px' }"
>
+{{ extraCount }}
</span>
</span>
<!-- Names -->
<span class="text-6 text-fg-muted font-light">{{ formattedAuthorNames }}</span>
</div>
</div>
</OgLayout>
</template>
Loading
Loading