Skip to content

Commit 798bf8e

Browse files
authored
Merge branch 'main' into fix/docs-page-improvements
2 parents 1b0b2b4 + 18db337 commit 798bf8e

File tree

31 files changed

+3561
-279
lines changed

31 files changed

+3561
-279
lines changed

.storybook/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const config = {
99
'storybook-i18n',
1010
],
1111
framework: '@storybook-vue/nuxt',
12-
staticDirs: ['./.public'],
12+
staticDirs: ['./.public', { from: '../public', to: '/' }],
1313
features: {
1414
backgrounds: false,
1515
},

.storybook/preview-head.html

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,61 @@
11
<style>
2+
/* Load Geist fonts to match the production app (normally handled by @nuxt/fonts,
3+
which is disabled in Storybook at this time) */
4+
@font-face {
5+
font-family: 'Geist';
6+
font-weight: 400;
7+
font-style: normal;
8+
font-display: swap;
9+
src: url('/fonts/Geist-Regular.ttf') format('truetype');
10+
}
11+
@font-face {
12+
font-family: 'Geist';
13+
font-weight: 500;
14+
font-style: normal;
15+
font-display: swap;
16+
src: url('/fonts/Geist-Medium.ttf') format('truetype');
17+
}
18+
@font-face {
19+
font-family: 'Geist';
20+
font-weight: 600;
21+
font-style: normal;
22+
font-display: swap;
23+
src: url('/fonts/Geist-SemiBold.ttf') format('truetype');
24+
}
25+
@font-face {
26+
font-family: 'Geist';
27+
font-weight: 700;
28+
font-style: normal;
29+
font-display: swap;
30+
src: url('/fonts/Geist-Bold.ttf') format('truetype');
31+
}
32+
@font-face {
33+
font-family: 'Geist Mono';
34+
font-weight: 400;
35+
font-style: normal;
36+
font-display: swap;
37+
src: url('/fonts/GeistMono-Regular.ttf') format('truetype');
38+
}
39+
@font-face {
40+
font-family: 'Geist Mono';
41+
font-weight: 500;
42+
font-style: normal;
43+
font-display: swap;
44+
src: url('/fonts/GeistMono-Medium.ttf') format('truetype');
45+
}
46+
@font-face {
47+
font-family: 'Geist Mono';
48+
font-weight: 700;
49+
font-style: normal;
50+
font-display: swap;
51+
src: url('/fonts/GeistMono-Bold.ttf') format('truetype');
52+
}
53+
html {
54+
-webkit-font-smoothing: antialiased;
55+
-moz-osx-font-smoothing: grayscale;
56+
text-rendering: optimizeLegibility;
57+
}
58+
259
/* Override docs story canvas background to match npmx theme */
360
.docs-story {
461
background-color: var(--bg, oklch(0.171 0 0)) !important;

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ We welcome contributions &ndash; please do feel free to explore the project and
148148

149149
## Related projects
150150

151-
- [npmx-replace-extension](https://github.com/tylersayshi/npmx-replace-extension) &ndash; Browser extension to redirect npmjs.com to npmx.dev (Chrome only for now)
151+
- [npmx-redirect](https://github.com/iaverages/npmx-redirect) &ndash; Browser extension that automatically redirects npmjs.com URLs to npmx.dev.
152152
- [JSR](https://jsr.io/) &ndash; The open-source package registry for modern JavaScript and TypeScript
153153
- [npm-userscript](https://github.com/bluwy/npm-userscript) &ndash; Browser userscript with various improvements and fixes for npmjs.com
154154
- [npm-alt](https://npm.willow.sh/) &ndash; An alternative npm package browser
@@ -158,7 +158,6 @@ We welcome contributions &ndash; please do feel free to explore the project and
158158
- [nxjt](https://nxjt.netlify.app) &ndash; npmx Jump To: Quickly navigate to npmx common webpages.
159159
- [npmx-weekly](https://npmx-weekly.trueberryless.org/) &ndash; 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).
160160
- [npmx-digest](https://npmx-digest.trueberryless.org/) &ndash; An automated news aggregation website that summarizes npmx activity from GitHub and Bluesky every 8 hours.
161-
- [npmx-redirect](https://github.com/iaverages/npmx-redirect) &ndash; Browser extension that automatically redirects npmjs.com URLs to npmx.dev.
162161
- [npmx-badge](https://npmx-badge.vercel.app/) &ndash; A playground to help you create custom badges quickly.
163162

164163
If you're building something cool, let us know! 🙏
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<script setup lang="ts">
2+
import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline'
3+
import { useCssVariables } from '~/composables/useColors'
4+
import {
5+
type VueUiSparklineConfig,
6+
type VueUiSparklineDatasetItem,
7+
type VueUiXyDatasetItem,
8+
} from 'vue-data-ui'
9+
import { getPalette, lightenColor } from 'vue-data-ui/utils'
10+
11+
import('vue-data-ui/style.css')
12+
13+
const props = defineProps<{
14+
dataset?: Array<
15+
VueUiXyDatasetItem & {
16+
color?: string
17+
series: number[]
18+
dashIndices?: number[]
19+
}
20+
>
21+
dates: number[]
22+
datetimeFormatterOptions: {
23+
year: string
24+
month: string
25+
day: string
26+
}
27+
showLastDatapointEstimation: boolean
28+
}>()
29+
30+
const { locale } = useI18n()
31+
const colorMode = useColorMode()
32+
const resolvedMode = shallowRef<'light' | 'dark'>('light')
33+
const rootEl = shallowRef<HTMLElement | null>(null)
34+
const palette = getPalette('')
35+
36+
const step = ref(0)
37+
38+
onMounted(() => {
39+
rootEl.value = document.documentElement
40+
})
41+
42+
watch(
43+
() => colorMode.value,
44+
value => {
45+
resolvedMode.value = value === 'dark' ? 'dark' : 'light'
46+
},
47+
{ flush: 'sync', immediate: true },
48+
)
49+
50+
const { colors } = useCssVariables(
51+
[
52+
'--bg',
53+
'--fg',
54+
'--bg-subtle',
55+
'--bg-elevated',
56+
'--border-hover',
57+
'--fg-subtle',
58+
'--border',
59+
'--border-subtle',
60+
],
61+
{
62+
element: rootEl,
63+
watchHtmlAttributes: true,
64+
watchResize: false, // set to true only if a var changes color on resize
65+
},
66+
)
67+
68+
const isDarkMode = computed(() => resolvedMode.value === 'dark')
69+
70+
const datasets = computed<VueUiSparklineDatasetItem[][]>(() => {
71+
return (props.dataset ?? []).map(unit => {
72+
return props.dates.map((period, i) => {
73+
return {
74+
period,
75+
value: unit.series[i] ?? 0,
76+
}
77+
})
78+
})
79+
})
80+
81+
const selectedIndex = ref<number | undefined | null>(null)
82+
83+
function hoverIndex({ index }: { index: number | undefined | null }) {
84+
if (typeof index === 'number') {
85+
selectedIndex.value = index
86+
}
87+
}
88+
89+
function resetHover() {
90+
selectedIndex.value = null
91+
step.value += 1 // required to reset all chart instances
92+
}
93+
94+
const configs = computed(() => {
95+
return (props.dataset || []).map<VueUiSparklineConfig>((unit, i) => {
96+
const lastIndex = unit.series.length - 1
97+
const dashIndices = props.showLastDatapointEstimation
98+
? Array.from(new Set([...(unit.dashIndices ?? []), lastIndex]))
99+
: unit.dashIndices
100+
101+
// Ensure we loop through available palette colours when the series count is higher than the avalable palette
102+
const fallbackColor = palette[i] ?? palette[i % palette.length] ?? palette[0]!
103+
const seriesColor = unit.color ?? fallbackColor
104+
const lightenedSeriesColor: string = unit.color
105+
? (lightenOklch(unit.color, 0.5) ?? seriesColor)
106+
: (lightenColor(seriesColor, 0.5) ?? seriesColor) // palette uses hex colours
107+
108+
return {
109+
a11y: {
110+
translations: {
111+
keyboardNavigation: $t(
112+
'package.trends.chart_assistive_text.keyboard_navigation_horizontal',
113+
),
114+
tableAvailable: $t('package.trends.chart_assistive_text.table_available'),
115+
tableCaption: $t('package.trends.chart_assistive_text.table_caption'),
116+
},
117+
},
118+
theme: isDarkMode.value ? 'dark' : '',
119+
temperatureColors: {
120+
show: isDarkMode.value,
121+
colors: [lightenedSeriesColor, seriesColor],
122+
},
123+
skeletonConfig: {
124+
style: {
125+
backgroundColor: 'transparent',
126+
dataLabel: {
127+
show: true,
128+
color: 'transparent',
129+
},
130+
area: {
131+
color: colors.value.borderHover,
132+
useGradient: false,
133+
opacity: 10,
134+
},
135+
line: {
136+
color: colors.value.borderHover,
137+
},
138+
},
139+
},
140+
skeletonDataset: Array.from({ length: unit.series.length }, () => 0),
141+
style: {
142+
backgroundColor: 'transparent',
143+
animation: { show: false },
144+
area: {
145+
color: colors.value.borderHover,
146+
useGradient: false,
147+
opacity: 10,
148+
},
149+
dataLabel: {
150+
offsetX: -12,
151+
fontSize: 24,
152+
bold: false,
153+
color: colors.value.fg,
154+
datetimeFormatter: {
155+
enable: true,
156+
locale: locale.value,
157+
useUTC: true,
158+
options: props.datetimeFormatterOptions,
159+
},
160+
},
161+
line: {
162+
color: seriesColor,
163+
dashIndices,
164+
dashArray: 3,
165+
},
166+
plot: {
167+
radius: 6,
168+
stroke: isDarkMode.value ? 'oklch(0.985 0 0)' : 'oklch(0.145 0 0)',
169+
},
170+
title: {
171+
fontSize: 12,
172+
color: colors.value.fgSubtle,
173+
bold: false,
174+
},
175+
176+
verticalIndicator: {
177+
strokeDasharray: 0,
178+
color: colors.value.fgSubtle,
179+
},
180+
padding: {
181+
left: 0,
182+
right: 0,
183+
top: 0,
184+
bottom: 0,
185+
},
186+
},
187+
}
188+
})
189+
})
190+
</script>
191+
192+
<template>
193+
<div class="grid gap-8 sm:grid-cols-2">
194+
<ClientOnly v-for="(config, i) in configs" :key="`config_${i}`">
195+
<div @mouseleave="resetHover" @keydown.esc="resetHover" class="w-full max-w-[400px] mx-auto">
196+
<div class="flex gap-2 place-items-center">
197+
<div class="h-3 w-3">
198+
<svg viewBox="0 0 2 2" class="w-full">
199+
<rect
200+
x="0"
201+
y="0"
202+
width="2"
203+
height="2"
204+
rx="0.3"
205+
:fill="dataset?.[i]?.color ?? palette[i]"
206+
/>
207+
</svg>
208+
</div>
209+
{{ applyEllipsis(dataset?.[i]?.name ?? '', 28) }}
210+
</div>
211+
<VueUiSparkline
212+
:key="`${i}_${step}`"
213+
:config
214+
:dataset="datasets?.[i]"
215+
:selectedIndex
216+
@hoverIndex="hoverIndex"
217+
>
218+
<!-- Keyboard navigation hint -->
219+
<template #hint="{ isVisible }">
220+
<p v-if="isVisible" class="text-accent text-xs text-center mt-2" aria-hidden="true">
221+
{{ $t('package.downloads.sparkline_nav_hint') }}
222+
</p>
223+
</template>
224+
225+
<template #skeleton>
226+
<!-- This empty div overrides the default built-in scanning animation on load -->
227+
<div />
228+
</template>
229+
</VueUiSparkline>
230+
</div>
231+
232+
<template #fallback>
233+
<!-- Skeleton matching VueUiSparkline layout (title 24px + SVG aspect 500:80) -->
234+
<div class="max-w-xs">
235+
<!-- Title row: fontSize * 2 = 24px -->
236+
<div class="h-6 flex items-center ps-3">
237+
<SkeletonInline class="h-3 w-36" />
238+
</div>
239+
<!-- Chart area: matches SVG viewBox 500:80 -->
240+
<div class="aspect-[500/80] flex items-center">
241+
<!-- Data label (covers ~42% width, matching dataLabel.offsetX) -->
242+
<div class="w-[42%] flex items-center ps-0.5">
243+
<SkeletonInline class="h-7 w-24" />
244+
</div>
245+
<!-- Sparkline line placeholder -->
246+
<div class="flex-1 flex items-end pe-3">
247+
<SkeletonInline class="h-px w-full" />
248+
</div>
249+
</div>
250+
</div>
251+
</template>
252+
</ClientOnly>
253+
</div>
254+
</template>

app/components/Compare/PackageSelector.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const props = defineProps<{
88
max?: number
99
}>()
1010
11-
const maxPackages = computed(() => props.max ?? 4)
11+
const maxPackages = computed(() => props.max ?? MAX_PACKAGE_SELECTION)
1212
1313
// Input state
1414
const inputValue = shallowRef('')

0 commit comments

Comments
 (0)