Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
e145c14
chore(deps): update dependency std-env to v4 (#2095)
renovate[bot] Mar 16, 2026
8da9e96
chore(deps): update devdependency oxlint to v1.55.0 (#2092)
renovate[bot] Mar 16, 2026
82ab52c
fix: fix zindex issue of button in header (#2081)
ShroXd Mar 16, 2026
6321dae
fix: avoid using slash as a separator between two numbers (#2087)
serhalp Mar 16, 2026
105898a
chore(deps): update devdependency oxlint to v1.56.0 (#2099)
renovate[bot] Mar 16, 2026
deec5b5
feat: add version history page (#2025)
ShroXd Mar 16, 2026
07a9ea9
fix(i18n): add missing pt-BR translations (#2090)
offeringofpie Mar 16, 2026
32ae83d
fix(i18n): complete missing version translations for pt-BR (#2103)
marlonwq Mar 16, 2026
31db8f8
fix: added scroll preservation opt in to the compare page (#2101)
mihaizaurus Mar 16, 2026
d9a3178
chore: add `bun.lock` for Bun file file association (#2107)
RiskyMH Mar 16, 2026
c5202d0
feat(ui): add multi-select and bulk actions for packages (#1672)
MatteoGabriele Mar 17, 2026
8ee186a
chore(i18n): sync pt-BR key order and formatting (#2104)
marlonwq Mar 17, 2026
3fc9ebd
docs: add npmx badge builder to readme related projects section (#2093)
yuyinws Mar 17, 2026
a0e8534
fix(ui): remove redundant back button in selection-view (#2119)
MatteoGabriele Mar 17, 2026
ad2b174
feat: opt-in repeated pulse animation on download graph (#2121)
trueberryless Mar 17, 2026
6fdb812
fix: improve normalizeGitUrl (#2113)
bluwy Mar 18, 2026
a482999
chore: update knip config & remove unused code (#2126)
webpro Mar 18, 2026
b66749e
feat(ui): show maintainers avatars (#2055)
bluwy Mar 18, 2026
b4b9cc8
fix(ui): correct show-scroll-to-top logic (#2127)
alexdln Mar 18, 2026
da2cf3e
fix: handle CLS issues in chart modal (#2032)
graphieros Mar 18, 2026
68d95cd
fix(a11y): move readme after sidebar in DOM order (#1072)
knowler Mar 18, 2026
88f4f4e
chore(deps): upgrade to vite-plus 0.1.12 and vite 8 (#2129)
danielroe Mar 18, 2026
d4dae8d
chore(deps): update pnpm/action-setup action to v5 (#2132)
renovate[bot] Mar 18, 2026
e8b51b0
refactor: use import aliases in test files (#2130)
shuuji3 Mar 18, 2026
62c2dbb
chore(deps): update all non-major dependencies (#2000)
renovate[bot] Mar 18, 2026
77c4570
fix: reword meta copy that compares npmx to npmjs.com (#2088)
serhalp Mar 18, 2026
5d8fcf5
fix: normalize metrics language across possible values (#2138)
bnb Mar 18, 2026
69dc57a
fix(i18n): add missing translation keys (#2140)
nadaniels Mar 19, 2026
230b7c7
fix: correctly parse package name for scoped packages (#2135)
gameroman Mar 19, 2026
62a9db0
fix(i18n): missing number formatting (#2158)
DDeenis Mar 20, 2026
a0dd2a9
fix(i18n): update Simplified Chinese and Traditional Chinese translat…
wChenonly Mar 20, 2026
399f7f9
chore: migrate deprecated typescript.tsdk to js/ts.tsdk.path (#2160)
RYGRIT Mar 20, 2026
5f037b7
fix: lower z-index of file path header to prevent overlapping version…
RYGRIT Mar 20, 2026
a9d69ae
docs(ui): add stories for About page (#2149)
cylewaitforit Mar 20, 2026
5e2e09e
fix: tag sorting on package versions page (#2105)
ShroXd Mar 20, 2026
fea4400
docs(ui): add dark theme for storybook docs (#2062)
cylewaitforit Mar 20, 2026
f0ada7e
fix(i18n): add missing translations for zh-CN (#2084)
ShroXd Mar 20, 2026
9ae5629
chore: revert "docs(ui): add dark theme for storybook docs (#2062)" (…
ghostdevv Mar 20, 2026
4ad0679
feat(a11y): use seeded patterns in bar charts (#2157)
graphieros Mar 20, 2026
8ae65d5
test(ui): set static values for storybook (#2174)
cylewaitforit Mar 20, 2026
3f2b865
docs(ui): add dark theme for storybook docs v2 (#2172)
cylewaitforit Mar 20, 2026
1cd901f
docs(ui): use storybook-i18n addon for locale switching (#2125)
cylewaitforit Mar 20, 2026
882dcac
fix(blog): ui improvements and comment layout fix (#2139)
Adebesin-Cell Mar 21, 2026
d2b21b1
feat: improve i18n (lunaria) status page (#2064)
alex-key Mar 21, 2026
a697f15
fix: change readme md renderer behavior (#1776)
RYGRIT Mar 21, 2026
c17f0f5
docs(ui): add ButtonGroup stories to storybook (#1964)
cylewaitforit Mar 21, 2026
49aba47
feat(ui): make version colors consistent (#2171)
MatteoGabriele Mar 21, 2026
171d446
feat: add WASM label (#2154)
gameroman Mar 21, 2026
c45e5ef
fix(ui): preview mode show the pr number (#2018)
btea Mar 21, 2026
291c22d
fix: don't diff install size with first version (#2175)
43081j Mar 21, 2026
8cd4074
feat: display yaml frontmatter as a table (#2166)
RYGRIT Mar 21, 2026
eea12a9
fix(ui): skeleton design not aligned between states (#2177)
MatteoGabriele Mar 21, 2026
7c1cea8
feat(ui): move fund to the package header (#2170)
MatteoGabriele Mar 21, 2026
2501b3c
feat(docs): add custom badge generator (#2152)
trueberryless Mar 21, 2026
b43cc0a
refactor: create a backbutton component (#2024)
epifaniofrancisco Mar 21, 2026
d86c9c4
fix(i18n): extract untranslated hardcoded strings (#2181)
serhalp Mar 21, 2026
fbe0b26
feat(docs): add links to advanced badges generator (#2183)
trueberryless Mar 21, 2026
fbf01bd
fix(ui): disambiguate the two "compare" links (#2180)
serhalp Mar 21, 2026
022d207
feat: add package download button (#1586)
Adebesin-Cell Mar 21, 2026
4294ada
feat: add animation to like button (#2082)
ShroXd Mar 21, 2026
5924009
chore: align base components size prop values (#2069)
antoineneff Mar 22, 2026
5dec2ef
fix(ui): remove install package command version for latest (#2039)
bluwy Mar 22, 2026
7f2fc1a
refactor: rename component folder names for consistency (#2019)
iiio2 Mar 22, 2026
7083d6a
chore(deps): update devdependency @e18e/eslint-plugin to v0.3.0 (#2222)
renovate[bot] Mar 23, 2026
6cb62c6
test(ui): add tests for mobile menu default closed state (#2195)
howwohmm Mar 23, 2026
c58ac2e
chore(deps): lock file maintenance (#2224)
renovate[bot] Mar 23, 2026
f0f31e5
chore(deps): update codecov/codecov-action digest to 1af5884 (#2218)
renovate[bot] Mar 23, 2026
deee8f2
fix(i18n): add missing translations for mr-IN (#2220)
trivikr Mar 23, 2026
376ccec
feat: charts a11y improvements (#2193)
graphieros Mar 23, 2026
5ba693c
fix(cli): force npmjs registry for login and npm cli operations (#2213)
MathurAditya724 Mar 23, 2026
39c2a3b
fix(ui): remove empty space on the package page when the readme is sh…
howwohmm Mar 23, 2026
32ba285
fix: connector modal should show pnpm dlx command instead of exec (#2…
ghostdevv Mar 23, 2026
1824c62
fix: align dependency warning icons to middle (#2227)
iiio2 Mar 23, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ We welcome contributions – please do feel free to explore the project and
- [npmx-weekly](https://npmx-weekly.trueberryless.org/) – A weekly newsletter for the npmx ecosystem. Add your own content via suggestions in the weekly PR on [GitHub](https://github.com/trueberryless-org/npmx-weekly/pulls?q=is%3Aopen+is%3Apr+label%3A%22%F0%9F%95%94+weekly+post%22).
- [npmx-digest](https://npmx-digest.trueberryless.org/) – An automated news aggregation website that summarizes npmx activity from GitHub and Bluesky every 8 hours.
- [npmx-redirect](https://github.com/iaverages/npmx-redirect) – Browser extension that automatically redirects npmjs.com URLs to npmx.dev.
- [npmx-badge](https://npmx-badge.vercel.app/) – A playground to help you create custom badges quickly.

If you're building something cool, let us know! 🙏

Expand Down
2 changes: 2 additions & 0 deletions app/components/BaseCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
defineProps<{
/** Whether this is an exact match for the query */
isExactMatch?: boolean
selected?: boolean
}>()
</script>

Expand All @@ -10,6 +11,7 @@ defineProps<{
class="group bg-bg-subtle border border-border rounded-lg p-4 sm:p-6 transition-[border-color,background-color] duration-200 hover:(border-border-hover bg-bg-muted) cursor-pointer relative focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50 focus-within:bg-bg-muted focus-within:border-border-hover"
:class="{
'border-accent/30 contrast-more:border-accent/90 bg-accent/5': isExactMatch,
'bg-fg-subtle/15!': selected,
}"
>
<!-- Glow effect for exact matches -->
Expand Down
1 change: 1 addition & 0 deletions app/components/ColumnPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const columnLabels = computed(() => ({
maintenanceScore: $t('filters.columns.maintenance_score'),
combinedScore: $t('filters.columns.combined_score'),
security: $t('filters.columns.security'),
selection: $t('filters.columns.selection'),
}))

function getColumnLabel(id: ColumnId): string {
Expand Down
87 changes: 87 additions & 0 deletions app/components/Package/ActionBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script setup lang="ts">
const { selectedPackages, selectedPackagesParam, clearSelectedPackages } = usePackageSelection()

const shortcutKey = 'b'
const actionBar = useTemplateRef('actionBarRef')
onKeyStroke(
e => {
const target = e.target as HTMLElement
const isCheckbox = target.hasAttribute('data-package-card-checkbox')
return isKeyWithoutModifiers(e, shortcutKey) && (!isEditableElement(target) || isCheckbox)
},
e => {
if (selectedPackages.value.length === 0) {
return
}

e.preventDefault()
actionBar.value?.focus()
},
)
</script>

<template>
<Transition name="action-bar-slide" appear>
<section
v-if="selectedPackages.length"
aria-labelledby="action-bar-title"
class="group fixed bottom-10 inset-is-0 w-full flex items-center justify-center z-36 pointer-events-none focus:outline-none"
tabindex="-1"
aria-keyshortcuts="b"
ref="actionBarRef"
>
<h3 id="action-bar-title" class="sr-only">
{{ $t('action_bar.title') }}
</h3>
<div
class="group-focus:outline-accent group-focus:outline-2 group-focus:outline-offset-2 pointer-events-auto bg-bg shadow-2xl shadow-accent/20 border-2 border-accent/60 p-3 min-w-[300px] rounded-xl flex gap-3 items-center justify-between animate-in ring-1 ring-accent/30"
>
<div aria-live="polite" aria-atomic="true" class="sr-only">
{{ $t('action_bar.selection', selectedPackages.length) }}.
{{ $t('action_bar.shortcut', { key: shortcutKey }) }}.
</div>

<div class="flex items-center gap-2">
<span class="text-fg font-semibold text-sm flex items-center gap-1.5">
{{ $t('action_bar.selection', selectedPackages.length) }}
</span>
<button
@click="clearSelectedPackages"
class="flex items-center ms-1 text-fg-muted hover:(text-fg bg-accent/10) p-1.5 rounded-lg transition-colors"
:aria-label="$t('action_bar.button_close_aria_label')"
>
<span class="i-lucide:x text-sm" aria-hidden="true" />
</button>
</div>

<LinkBase
:to="{ name: 'compare', query: { packages: selectedPackagesParam } }"
variant="button-secondary"
classicon="i-lucide:git-compare"
>
{{ $t('package.links.compare') }}
</LinkBase>
</div>
</section>
</Transition>
</template>

<style scoped>
/* Action bar slide/fade animation */
.action-bar-slide-enter-active,
.action-bar-slide-leave-active {
transition:
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.action-bar-slide-enter-from,
.action-bar-slide-leave-to {
opacity: 0;
transform: translateY(40px) scale(0.98);
}
.action-bar-slide-enter-to,
.action-bar-slide-leave-from {
opacity: 1;
transform: translateY(0) scale(1);
}
</style>
53 changes: 23 additions & 30 deletions app/components/Package/Card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const props = defineProps<{
searchQuery?: string
}>()

const { isPackageSelected, togglePackageSelection, canSelectMore } = usePackageSelection()
const isSelected = computed<boolean>(() => {
return isPackageSelected(props.result.package.name)
})

const emit = defineEmits<{
clickKeyword: [keyword: string]
}>()
Expand All @@ -39,16 +44,16 @@ const numberFormatter = useNumberFormatter()
</script>

<template>
<BaseCard :isExactMatch="isExactMatch">
<div class="mb-2 flex items-baseline justify-start gap-2">
<BaseCard :selected="isSelected" :isExactMatch="isExactMatch">
<header class="mb-4 flex items-baseline justify-between gap-2">
<component
:is="headingLevel ?? 'h3'"
class="font-mono text-sm sm:text-base font-medium text-fg group-hover:text-fg transition-colors duration-200 min-w-0 break-all"
>
<NuxtLink
:to="packageRoute(result.package.name)"
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
class="decoration-none scroll-mt-48 scroll-mb-6 after:content-[''] after:absolute after:inset-0"
class="decoration-none after:content-[''] after:absolute after:inset-0"
:data-result-index="index"
dir="ltr"
>{{ result.package.name }}</NuxtLink
Expand All @@ -59,28 +64,17 @@ const numberFormatter = useNumberFormatter()
>{{ $t('search.exact_match') }}</span
>
</component>
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
<!-- Mobile: version next to package name -->
<div class="sm:hidden text-fg-subtle flex items-center gap-1.5 shrink-0">
<span
v-if="result.package.version"
class="font-mono text-xs truncate max-w-20"
:title="result.package.version"
>
v{{ result.package.version }}
</span>
<ProvenanceBadge
v-if="result.package.publisher?.trustedPublisher"
:provider="result.package.publisher.trustedPublisher.id"
:package-name="result.package.name"
:version="result.package.version"
:linked="false"
compact
/>
</div>
</div>
<div class="flex justify-start items-start gap-4 sm:gap-8">
<div class="min-w-0">

<PackageSelectionCheckbox
:package-name="result.package.name"
:disabled="!canSelectMore && !isSelected"
:checked="isSelected"
@change="togglePackageSelection"
/>
</header>

<div class="flex flex-col sm:flex-row sm:justify-start sm:items-start gap-6 sm:gap-8">
<div class="min-w-0 w-full">
<p v-if="pkgDescription" class="text-fg-muted text-xs sm:text-sm line-clamp-2 mb-2 sm:mb-3">
<span v-html="pkgDescription" />
</p>
Expand Down Expand Up @@ -124,10 +118,9 @@ const numberFormatter = useNumberFormatter()
</div>
</dl>
</div>
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
<!-- Desktop: version and downloads on right side -->
<div class="hidden sm:flex flex-col gap-2 shrink-0">
<div class="text-fg-subtle flex items-start gap-2 justify-end">

<div class="flex flex-col gap-2 shrink-0">
<div class="text-fg-subtle flex items-start gap-2 sm:justify-end">
<span
v-if="result.package.version"
class="font-mono text-xs truncate max-w-32"
Expand All @@ -150,7 +143,7 @@ const numberFormatter = useNumberFormatter()
</div>
<div
v-if="result.downloads?.weekly"
class="text-fg-subtle gap-2 flex items-center justify-end"
class="text-fg-subtle gap-2 flex items-center sm:justify-end"
>
<span class="i-lucide:chart-line w-3.5 h-3.5" aria-hidden="true" />
<span class="font-mono text-xs">
Expand Down
12 changes: 6 additions & 6 deletions app/components/Package/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ const likeAction = async () => {

<template>
<!-- Package header -->
<header class="bg-bg pt-5 w-full container">
<header class="bg-bg pt-5 pb-1 w-full container">
<!-- Package name and version -->
<div class="flex items-baseline justify-between gap-x-2 gap-y-1 flex-wrap min-w-0">
<CopyToClipboardButton
Expand Down Expand Up @@ -319,7 +319,7 @@ const likeAction = async () => {
</header>
<div
ref="header"
class="w-full bg-bg sticky top-14 z-50 border-b border-border pt-2"
class="w-full bg-bg sticky top-14 z-10 border-b border-border pt-2"
:class="[$style.packageHeader]"
data-testid="package-subheader"
>
Expand Down Expand Up @@ -395,7 +395,7 @@ const likeAction = async () => {
v-if="mainLink"
:to="mainLink"
aria-keyshortcuts="m"
class="decoration-none border-b-2 p-1 hover:border-accent/50 lowercase"
class="decoration-none border-b-2 p-1 hover:border-accent/50 lowercase focus-visible:[outline-offset:-2px]!"
:class="page === 'main' ? 'border-accent text-accent!' : 'border-transparent'"
>
{{ $t('package.links.main') }}
Expand All @@ -404,7 +404,7 @@ const likeAction = async () => {
v-if="docsLink"
:to="docsLink"
aria-keyshortcuts="d"
class="decoration-none border-b-2 p-1 hover:border-accent/50"
class="decoration-none border-b-2 p-1 hover:border-accent/50 focus-visible:[outline-offset:-2px]!"
:class="page === 'docs' ? 'border-accent text-accent!' : 'border-transparent'"
>
{{ $t('package.links.docs') }}
Expand All @@ -413,7 +413,7 @@ const likeAction = async () => {
v-if="codeLink"
:to="codeLink"
aria-keyshortcuts="."
class="decoration-none border-b-2 p-1 hover:border-accent/50"
class="decoration-none border-b-2 p-1 hover:border-accent/50 focus-visible:[outline-offset:-2px]!"
:class="page === 'code' ? 'border-accent text-accent!' : 'border-transparent'"
>
{{ $t('package.links.code') }}
Expand All @@ -423,7 +423,7 @@ const likeAction = async () => {
:to="diffLink"
:title="$t('compare.compare_versions_title')"
aria-keyshortcuts="f"
class="decoration-none border-b-2 p-1 hover:border-accent/50"
class="decoration-none border-b-2 p-1 hover:border-accent/50 focus-visible:[outline-offset:-2px]!"
:class="page === 'diff' ? 'border-accent text-accent!' : 'border-transparent'"
>
{{ $t('compare.compare_versions') }}
Expand Down
3 changes: 2 additions & 1 deletion app/components/Package/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ defineExpose({
<template #default="{ item, index }">
<div class="pb-4">
<PackageCard
:result="item as NpmSearchResult"
:key="item.package.name"
:result="item"
:heading-level="headingLevel"
:show-publisher="showPublisher"
:index="index"
Expand Down
23 changes: 23 additions & 0 deletions app/components/Package/ListToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const pageSize = defineModel<PageSize>('pageSize', { required: true })

const emit = defineEmits<{
'toggleColumn': [columnId: ColumnId]
'toggleSelection': []
'resetColumns': []
'clearFilter': [chip: FilterChip]
'clearAllFilters': []
Expand Down Expand Up @@ -110,6 +111,8 @@ const sortKeyLabelKeys = computed<Record<SortKey, string>>(() => ({
function getSortKeyLabelKey(key: SortKey): string {
return sortKeyLabelKeys.value[key]
}

const { selectedPackages, clearSelectedPackages } = usePackageSelection()
</script>

<template>
Expand Down Expand Up @@ -211,6 +214,26 @@ function getSortKeyLabelKey(key: SortKey): string {

<ViewModeToggle v-model="viewMode" />
</div>

<div
class="flex items-center order-3 sm:border-is sm:border-fg-subtle/20 sm:ps-3"
v-if="selectedPackages.length"
>
<ButtonBase
variant="secondary"
@click="emit('toggleSelection')"
classicon="i-lucide:package-check"
>
{{ t('filters.view_selected') }} ({{ selectedPackages.length }})
</ButtonBase>
<button
@click="clearSelectedPackages"
aria-label="Close action bar"
class="flex items-center ms-2"
>
<span class="i-lucide:x text-sm" />
</button>
</div>
</div>
</div>

Expand Down
43 changes: 43 additions & 0 deletions app/components/Package/SelectionCheckbox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
const props = defineProps<{
packageName: string
checked: boolean
disabled?: boolean
}>()

const emit = defineEmits<{
(e: 'change', packageName: string): void
}>()

const { t } = useI18n()
const disabledText = t('package.card.select_maximum', MAX_PACKAGE_SELECTION)
</script>

<template>
<div class="relative z-1">
<label>
<span class="sr-only" v-if="disabled">{{ disabledText }}</span>
<span class="sr-only" v-else> {{ $t('package.card.select') }}: {{ packageName }} </span>

<TooltipApp v-if="disabled" :text="disabledText" position="top">
<input
class="opacity-0 group-hover:opacity-100 size-4 accent-accent border border-fg-muted/30 hover:cursor-not-allowed"
:class="{ 'opacity-100! disabled:opacity-30!': isTouchDevice() }"
type="checkbox"
:disabled
/>
</TooltipApp>

<input
v-else
data-package-card-checkbox
class="opacity-0 group-focus-within:opacity-100 checked:opacity-100 group-hover:opacity-100 size-4 cursor-pointer accent-accent border border-fg-muted/30 hover:border-accent transition-colors disabled:group-hover:opacity-30 disabled:hover:cursor-not-allowed"
:class="{ 'opacity-100! disabled:opacity-30!': isTouchDevice() }"
type="checkbox"
:checked
:disabled
@change="emit('change', packageName)"
/>
</label>
</div>
</template>
Loading
Loading