Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
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