Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
fee116c
chore: bump vue-data-ui from 3.15.9 to 3.15.10 (#1957)
graphieros Mar 6, 2026
0d952fa
fix(a11y): improve toggle switches (#1782)
userquin Mar 6, 2026
b6da02d
fix: fix og image generation errors (#1982)
alexdln Mar 7, 2026
03f7ecd
chore: bump `lint-staged` to `16.3.2` (#1965)
userquin Mar 7, 2026
250127a
fix: update handles for patak (#1989)
vladh Mar 8, 2026
c2e7230
fix: remove border in package suggestion dropdown items (#1970)
iiio2 Mar 8, 2026
48df372
fix(i18n): update Russian translations (#1966)
dragomano Mar 8, 2026
e91c760
chore(deps): update devdependency knip to 5.86.0 and remove unneeded …
cylewaitforit Mar 8, 2026
e374f75
fix(ui): the install command should use the correct version (#1958)
btea Mar 8, 2026
bd73ea9
feat: add bar chart view to compare page (#1974)
graphieros Mar 8, 2026
ed7f8f2
fix: the weekly data anomaly detection was broken for the Svelte anom…
samal-rasmussen Mar 8, 2026
d26e250
chore(i18n): improve french translations (#1994)
huang-julien Mar 8, 2026
2f39f4e
chore(deps): update devdependency eslint-plugin-regexp to v3.1.0 (#2004)
renovate[bot] Mar 9, 2026
089a73b
refactor: remove redundant title attributes from package nav (#1999)
knowler Mar 9, 2026
36bebce
feat: improve badge customization with dynamic text based on contrast…
sandros94 Mar 9, 2026
8baf1a5
fix: various chart improvements (#2003)
graphieros Mar 9, 2026
bbb937b
fix: use resolvedVersion on package page to display data correctly (#…
alex-key Mar 9, 2026
04f3ab9
feat(i18n): localise translations for other git providers (#1962)
WilcoSp Mar 9, 2026
8cea27f
chore(deps): update dependency vue to v3.5.30 (#2008)
renovate[bot] Mar 9, 2026
11842b4
fix: code line highlighting not working with the light theme (#2015)
RYGRIT Mar 10, 2026
ef0f696
fix(i18n): update Czech translations (#2006)
VentyCZ Mar 10, 2026
68ddbfc
feat(ui): highlight slash keyboard shortcut on header/home search bar…
aqandrew Mar 10, 2026
0d82d42
fix: data anomalies in chart (#2012)
jycouet Mar 10, 2026
99c9533
fix(i18n): update Azerbaijani translations (#1991)
chz Mar 10, 2026
87f3aaf
fix: weekly chart prediction and data pipeline extraction (#2014)
jycouet Mar 10, 2026
3712560
fix(ui): versions modal usage button copy (#2007)
MatteoGabriele Mar 10, 2026
3de681d
feat(a11y): add reusable Alert component (#1955)
ShroXd Mar 11, 2026
d3cfce5
feat: add binary file warning (#1959)
ShroXd Mar 11, 2026
ad3ea72
fix: prevent Chrome Android date input value clipping (#2037)
lukewarlow Mar 12, 2026
41b84d5
fix(i18n): update polish translations (#2029)
mikouaji Mar 12, 2026
7d697a2
refactor: move package-header to new component (#2030)
alexdln Mar 12, 2026
e0a31c3
fix(ui): remove redundant calendar icons from date inputs (#2042)
marlonwq Mar 12, 2026
d85c65b
chore: bump vue-data-ui from 3.15.11 to 3.15.12 (#2040)
graphieros Mar 12, 2026
eda2657
feat(i18n): complete Indonesian (id-ID) translations (#2050)
ddtamn Mar 12, 2026
0d0707f
feat: package header UI (#2035)
alexdln Mar 12, 2026
8976969
fix: align weekly chart buckets from end to match npm downloads (#2052)
jycouet Mar 13, 2026
b23897c
fix: remove duplicate addDays utility (#2059)
graphieros Mar 13, 2026
44ad364
fix: pds page style bug (#1861)
RYGRIT Mar 13, 2026
adb3959
chore: remove auto-imported imports (#2061)
MatteoGabriele Mar 13, 2026
5db5dc6
refactor: move duplicated repositoryUrl in composable (#2060)
MatteoGabriele Mar 13, 2026
aad47ed
fix(i18n): update Japanese translations (#2066)
shuuji3 Mar 13, 2026
9e8c805
feat(i18n): fix some translation error and add missing key for indone…
adeak-bar25 Mar 13, 2026
a170292
fix: always align weekly chart buckets from range end date (#2071)
jycouet Mar 14, 2026
a444140
refactor(ui): move package external links to component (#2077)
MatteoGabriele Mar 15, 2026
b36778f
feat(i18n): improve UA translations, add missing keys (#2073)
alex-key Mar 15, 2026
c784a68
fix: create packages being broken for scoped packages (#2079)
lino-levan Mar 15, 2026
59b67e2
feat(i18n): use rangeFormat for pagination (#2078)
kytta Mar 15, 2026
9342f6c
fix: refine compare grid header layout (#2072)
mihaizaurus Mar 15, 2026
6e8f2b2
fix: fetch resolved version for install size (#2086)
gameroman Mar 15, 2026
4422cba
refactor: improve AccentColorId type constraints (#1972)
maxchang3 Mar 15, 2026
7062d19
refactor: remove linter warnings (#2063)
MatteoGabriele Mar 15, 2026
d21b146
fix: use jsdelivr's `content-type` header to detect binary file (#2036)
shuuji3 Mar 15, 2026
fbb9011
feat(i18n): add pds, blog and missing Spanish translations (#2085)
userquin Mar 15, 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 CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ We agree to restrict the following behaviors in our community. Instances, threat

Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.

When an incident does occur, it is important to report it promptly. To report a possible violation, contact the project stewards (@danielroe and @patak.dev) by DM in our community chat. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project stewards are obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
When an incident does occur, it is important to report it promptly. To report a possible violation, contact the project stewards (@danielroe and @patak.cat) by DM in our community chat. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project stewards are obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution.

Expand Down
2 changes: 1 addition & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Not every contributor will reach this level, and that's okay! Maintainers still
The npmx project Stewards are currently:

- **Daniel Roe** ([website](https://roe.dev), [social](https://bsky.app/profile/danielroe.dev), [github](https://github.com/danielroe), [@danielroe](https://chat.npmx.dev))
- **Matias Capeletto** ([website](https://patak.dev), [social](https://bsky.app/profile/patak.dev), [github](https://github.com/patak-dev), [@patak.dev](https://chat.npmx.dev))
- **Matias Capeletto** ([website](https://patak.cat), [social](https://bsky.app/profile/patak.cat), [github](https://github.com/patak-cat), [@patak.cat](https://chat.npmx.dev))

---

Expand Down
318 changes: 318 additions & 0 deletions app/components/Compare/FacetBarChart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { VueUiHorizontalBar } from 'vue-data-ui/vue-ui-horizontal-bar'
import type {
VueUiHorizontalBarConfig,
VueUiHorizontalBarDatapoint,
VueUiHorizontalBarDatasetItem,
} from 'vue-data-ui'
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
import { drawSmallNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
import {
loadFile,
insertLineBreaks,
sanitise,
applyEllipsis,
copyAltTextForCompareFacetBarChart,
} from '~/utils/charts'

import('vue-data-ui/style.css')

const props = defineProps<{
values: (FacetValue | null | undefined)[]
packages: string[]
label: string
description: string
facetLoading?: boolean
}>()

const colorMode = useColorMode()
const resolvedMode = shallowRef<'light' | 'dark'>('light')
const rootEl = shallowRef<HTMLElement | null>(null)
const { width } = useElementSize(rootEl)
const { copy, copied } = useClipboard()

const mobileBreakpointWidth = 640
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)

const chartKey = ref(0)

const { colors } = useCssVariables(
[
'--bg',
'--fg',
'--bg-subtle',
'--bg-elevated',
'--fg-subtle',
'--fg-muted',
'--border',
'--border-subtle',
],
{
element: rootEl,
watchHtmlAttributes: true,
watchResize: false,
},
)

const watermarkColors = computed(() => ({
fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK,
bg: colors.value.bg ?? OKLCH_NEUTRAL_FALLBACK,
fgSubtle: colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK,
}))

onMounted(async () => {
rootEl.value = document.documentElement
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
})

watch(
() => colorMode.value,
value => {
resolvedMode.value = value === 'dark' ? 'dark' : 'light'
},
{ flush: 'sync' },
)

watch(
() => props.packages,
(newP, oldP) => {
if (newP.length !== oldP.length) return
chartKey.value += 1
},
)

const isDarkMode = computed(() => resolvedMode.value === 'dark')

const dataset = computed<VueUiHorizontalBarDatasetItem[]>(() => {
if (props.facetLoading) return []
return props.packages.map((name, index) => {
const rawValue = props.values[index]?.raw
return {
name: insertLineBreaks(applyEllipsis(name)),
value: typeof rawValue === 'number' ? rawValue : 0,
color: isListedFramework(name) ? getFrameworkColor(name) : undefined,
formattedValue: props.values[index]?.display,
}
})
})

const skeletonDataset = computed(() =>
props.packages.map((_pkg, i) => ({
name: '_',
value: i + 1,
color: colors.value.border,
})),
)

function buildExportFilename(extension: string): string {
const sanitizedPackages = props.packages.map(p => sanitise(p).slice(0, 10)).join('_')
const comparisonLabel = sanitise($t('compare.packages.section_comparison'))
const facetLabel = sanitise(props.label)
return `${facetLabel}_${comparisonLabel}_${sanitizedPackages}.${extension}`
}

const config = computed<VueUiHorizontalBarConfig>(() => {
return {
theme: isDarkMode.value ? 'dark' : '',
userOptions: {
buttons: {
tooltip: false,
pdf: false,
fullscreen: false,
sort: false,
annotator: false,
table: false,
csv: false,
altCopy: true,
},
buttonTitle: {
img: $t('package.trends.download_file', { fileType: 'PNG' }),
svg: $t('package.trends.download_file', { fileType: 'SVG' }),
altCopy: $t('package.trends.copy_alt.button_label'),
},
callbacks: {
img: args => {
const imageUri = args?.imageUri
if (!imageUri) return
loadFile(imageUri, buildExportFilename('png'))
},
svg: args => {
const blob = args?.blob
if (!blob) return
const url = URL.createObjectURL(blob)
loadFile(url, buildExportFilename('svg'))
URL.revokeObjectURL(url)
},
altCopy: ({ dataset: dst, config: cfg }) => {
copyAltTextForCompareFacetBarChart({
dataset: dst,
config: {
...cfg,
facet: props.label,
description: props.description,
copy,
$t,
},
})
},
},
},
skeletonDataset: skeletonDataset.value,
skeletonConfig: {
style: {
chart: {
backgroundColor: colors.value.bg,
},
},
},
style: {
chart: {
backgroundColor: colors.value.bg,
height: 60 * props.packages.length,
layout: {
bars: {
rowColor: isDarkMode.value ? colors.value.borderSubtle : colors.value.bgSubtle,
rowRadius: 4,
borderRadius: 4,
dataLabels: {
fontSize: isMobile.value ? 12 : 18,
percentage: { show: false },
offsetX: 12,
bold: false,
color: colors.value.fg,
value: {
formatter: ({ config }) => {
return config?.datapoint?.formattedValue ?? '0'
},
},
},
nameLabels: {
fontSize: isMobile.value ? 12 : 18,
color: colors.value.fgSubtle,
},
underlayerColor: colors.value.bg,
},
highlighter: {
opacity: isMobile.value ? 0 : 5,
},
},
legend: {
show: false,
},
title: {
fontSize: 16,
bold: false,
text: props.label,
color: colors.value.fg,
subtitle: {
text: props.description,
fontSize: 12,
color: colors.value.fgSubtle,
},
},
tooltip: {
show: !isMobile.value,
borderColor: 'transparent',
backdropFilter: false,
backgroundColor: 'transparent',
customFormat: ({ datapoint }) => {
const name = datapoint?.name?.replace(/\n/g, '<br>')
return `
<div class="font-mono p-3 border border-border rounded-md bg-[var(--bg)]/10 backdrop-blur-md">
<div class="grid grid-cols-[12px_minmax(0,1fr)_max-content] items-center gap-x-3">
<div class="w-3 h-3">
<svg viewBox="0 0 2 2" class="w-full h-full">
<rect x="0" y="0" width="2" height="2" rx="0.3" fill="${datapoint?.color}" />
</svg>
</div>
<span class="text-3xs uppercase tracking-wide text-[var(--fg)]/70 truncate">
${name}
</span>
<span class="text-base text-[var(--fg)] font-mono tabular-nums text-end">
${(datapoint as VueUiHorizontalBarDatapoint).formattedValue ?? 0}
</span>
</div>
</div>
`
},
},
},
},
}
})
</script>

<template>
<div class="font-mono facet-bar">
<ClientOnly v-if="dataset.length">
<VueUiHorizontalBar :key="chartKey" :dataset :config class="[direction:ltr]">
<template #svg="{ svg }">
<!-- Inject npmx logo & tagline during SVG and PNG print -->
<g
v-if="svg.isPrintingSvg || svg.isPrintingImg"
v-html="
drawSmallNpmxLogoAndTaglineWatermark({
svg,
colors: watermarkColors,
translateFn: $t,
})
"
/>
</template>

<template #menuIcon="{ isOpen }">
<span v-if="isOpen" class="i-lucide:x w-6 h-6" aria-hidden="true" />
<span v-else class="i-lucide:ellipsis-vertical w-6 h-6" aria-hidden="true" />
</template>
<template #optionCsv>
<span class="text-fg-subtle font-mono pointer-events-none">CSV</span>
</template>
<template #optionImg>
<span class="text-fg-subtle font-mono pointer-events-none">PNG</span>
</template>
<template #optionSvg>
<span class="text-fg-subtle font-mono pointer-events-none">SVG</span>
</template>
<template #optionAltCopy>
<span
class="w-6 h-6"
:class="
copied ? 'i-lucide:check text-accent' : 'i-lucide:person-standing text-fg-subtle'
"
style="pointer-events: none"
aria-hidden="true"
/>
</template>
</VueUiHorizontalBar>

<template #fallback>
<div class="flex flex-col gap-2 justify-center items-center mb-2">
<SkeletonInline class="h-4 w-16" />
<SkeletonInline class="h-4 w-28" />
</div>
<div class="flex flex-col gap-1">
<SkeletonInline class="h-7 w-full" v-for="pkg in packages" :key="pkg" />
</div>
</template>
</ClientOnly>

<template v-else>
<div class="flex flex-col gap-2 justify-center items-center mb-2">
<SkeletonInline class="h-4 w-16" />
<SkeletonInline class="h-4 w-28" />
</div>
<div class="flex flex-col gap-1">
<SkeletonInline class="h-7 w-full" v-for="pkg in packages" :key="pkg" />
</div>
</template>
</div>
</template>

<style>
.facet-bar .atom-subtitle {
width: 80% !important;
margin: 0 auto;
height: 2rem;
}
</style>
4 changes: 2 additions & 2 deletions app/components/Compare/PackageSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ onClickOutside(containerRef, () => {
<ButtonBase
v-if="showNoDependencyOption"
data-navigable
class="block w-full text-start"
class="block w-full text-start !border-transparent"
:class="highlightedIndex === 0 ? '!bg-accent/15' : ''"
:aria-label="$t('compare.no_dependency.add_column')"
@mouseenter="highlightedIndex = 0"
Expand All @@ -297,7 +297,7 @@ onClickOutside(containerRef, () => {
v-for="(result, index) in filteredResults"
:key="result.name"
data-navigable
class="block w-full text-start my-0.5"
class="block w-full text-start my-0.5 !border-transparent"
:class="highlightedIndex === index + resultIndexOffset ? '!bg-accent/15' : ''"
@mouseenter="highlightedIndex = index + resultIndexOffset"
@click="addPackage(result.name)"
Expand Down
2 changes: 2 additions & 0 deletions app/components/OgImage/BlogPost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ const formattedAuthorNames = computed(() => {
v-if="author.avatar"
:src="author.avatar"
:alt="author.name"
width="48"
height="48"
class="w-full h-full object-cover"
/>
<span v-else style="font-size: 20px; color: #666; font-weight: 500">
Expand Down
Loading
Loading