Skip to content

Commit 4d97d7f

Browse files
committed
Merge branch 'main' of github.com:harlan-zw/fork-npmx.dev into feat/og-image-v6
2 parents f2de5ad + 8ee186a commit 4d97d7f

92 files changed

Lines changed: 2862 additions & 1229 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.storybook/main.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ const config = {
88
features: {
99
backgrounds: false,
1010
},
11-
async viteFinal(config) {
12-
config.plugins ??= []
11+
async viteFinal(newConfig) {
12+
newConfig.plugins ??= []
1313

14-
config.plugins.push({
14+
newConfig.plugins.push({
1515
name: 'ignore-internals',
1616
transform(_, id) {
1717
if (id.includes('/app/pages/blog/') && id.endsWith('.md')) {
@@ -23,7 +23,7 @@ const config = {
2323
// vue-docgen-api can crash on components that import types from other
2424
// .vue files (it tries to parse the SFC with @babel/parser as plain TS).
2525
// This wrapper catches those errors so the build doesn't fail.
26-
const docgenPlugin = config.plugins?.find(
26+
const docgenPlugin = newConfig.plugins?.find(
2727
(p): p is Extract<typeof p, { name: string }> =>
2828
!!p && typeof p === 'object' && 'name' in p && p.name === 'storybook:vue-docgen-plugin',
2929
)
@@ -48,7 +48,7 @@ const config = {
4848
}
4949
}
5050

51-
return config
51+
return newConfig
5252
},
5353
} satisfies StorybookConfig
5454

app/components/BaseCard.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
defineProps<{
33
/** Whether this is an exact match for the query */
44
isExactMatch?: boolean
5+
selected?: boolean
56
}>()
67
</script>
78

@@ -10,6 +11,7 @@ defineProps<{
1011
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"
1112
:class="{
1213
'border-accent/30 contrast-more:border-accent/90 bg-accent/5': isExactMatch,
14+
'bg-fg-subtle/15!': selected,
1315
}"
1416
>
1517
<!-- Glow effect for exact matches -->

app/components/ColumnPicker.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const columnLabels = computed(() => ({
5454
maintenanceScore: $t('filters.columns.maintenance_score'),
5555
combinedScore: $t('filters.columns.combined_score'),
5656
security: $t('filters.columns.security'),
57+
selection: $t('filters.columns.selection'),
5758
}))
5859
5960
function getColumnLabel(id: ColumnId): string {

app/components/Compare/ComparisonGrid.vue

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const props = defineProps<{
1717
1818
/** Total column count including the optional no-dep column */
1919
const totalColumns = computed(() => props.columns.length + (props.showNoDependency ? 1 : 0))
20+
const visibleColumns = computed(() => Math.min(totalColumns.value, 4))
2021
2122
/** Compute plain-text tooltip for a replacement column */
2223
function getReplacementTooltip(col: ComparisonGridColumn): string {
@@ -30,32 +31,43 @@ function getReplacementTooltip(col: ComparisonGridColumn): string {
3031
<div class="overflow-x-auto">
3132
<div
3233
class="comparison-grid"
33-
:class="[totalColumns === 4 ? 'min-w-[800px]' : 'min-w-[600px]', `columns-${totalColumns}`]"
34-
:style="{ '--columns': totalColumns }"
34+
:style="{
35+
'--package-count': totalColumns,
36+
'--visible-columns': visibleColumns,
37+
}"
3538
>
3639
<!-- Header row -->
3740
<div class="comparison-header">
38-
<div class="comparison-label" />
41+
<div class="comparison-label relative bg-bg" />
3942

4043
<!-- Package columns -->
41-
<div v-for="col in columns" :key="col.name" class="comparison-cell comparison-cell-header">
42-
<span class="inline-flex items-center gap-1.5 truncate">
44+
<div
45+
v-for="col in columns"
46+
:key="col.name"
47+
class="comparison-cell comparison-cell-header min-w-0"
48+
>
49+
<div class="flex items-start justify-center gap-1.5 min-w-0">
4350
<LinkBase
4451
:to="packageRoute(col.name, col.version)"
45-
class="text-sm truncate"
46-
block
52+
class="flex min-w-0 flex-col items-center text-center text-sm"
4753
:title="col.version ? `${col.name}@${col.version}` : col.name"
4854
>
49-
{{ col.name }}<template v-if="col.version">@{{ col.version }}</template>
55+
<span class="min-w-0 break-words line-clamp-1">
56+
{{ col.name }}
57+
</span>
58+
<span v-if="col.version" class="text-fg-muted line-clamp-1">
59+
@{{ col.version }}
60+
</span>
5061
</LinkBase>
62+
5163
<TooltipApp v-if="col.replacement" :text="getReplacementTooltip(col)" position="bottom">
5264
<span
53-
class="i-lucide:lightbulb w-3.5 h-3.5 text-amber-500 shrink-0 cursor-help"
65+
class="i-lucide:lightbulb mt-0.5 h-3.5 w-3.5 shrink-0 cursor-help text-amber-500"
5466
role="img"
5567
:aria-label="$t('package.replacement.title')"
5668
/>
5769
</TooltipApp>
58-
</span>
70+
</div>
5971
</div>
6072

6173
<!-- "No dep" column (always last) -->
@@ -100,29 +112,30 @@ function getReplacementTooltip(col: ComparisonGridColumn): string {
100112

101113
<style scoped>
102114
.comparison-grid {
115+
--label-column-width: 140px;
116+
--package-column-width: calc((100% - var(--label-column-width)) / var(--visible-columns));
103117
display: grid;
104118
gap: 0;
105-
}
106-
107-
.comparison-grid.columns-2 {
108-
grid-template-columns: minmax(120px, 180px) repeat(2, 1fr);
109-
}
110-
111-
.comparison-grid.columns-3 {
112-
grid-template-columns: minmax(120px, 160px) repeat(3, 1fr);
113-
}
114-
115-
.comparison-grid.columns-4 {
116-
grid-template-columns: minmax(100px, 140px) repeat(4, 1fr);
119+
grid-template-columns:
120+
var(--label-column-width)
121+
repeat(var(--package-count), minmax(var(--package-column-width), var(--package-column-width)));
117122
}
118123
119124
.comparison-header {
120125
display: contents;
121126
}
122127
123128
.comparison-header > .comparison-label {
124-
padding: 0.75rem 1rem;
125-
border-bottom: 1px solid var(--color-border);
129+
z-index: 3;
130+
}
131+
132+
.comparison-label {
133+
position: sticky;
134+
left: 0;
135+
z-index: 2;
136+
inline-size: var(--label-column-width);
137+
min-inline-size: var(--label-column-width);
138+
isolation: isolate;
126139
}
127140
128141
.comparison-header > .comparison-cell-header {

app/components/Compare/FacetBarChart.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ const config = computed<VueUiHorizontalBarConfig>(() => {
178178
bold: false,
179179
color: colors.value.fg,
180180
value: {
181-
formatter: ({ config }) => {
182-
return config?.datapoint?.formattedValue ?? '0'
181+
formatter: ({ config: formatterConfig }) => {
182+
return formatterConfig?.datapoint?.formattedValue ?? '0'
183183
},
184184
},
185185
},

app/components/Compare/FacetRow.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ function isCellLoading(index: number): boolean {
8888
<template>
8989
<div class="contents">
9090
<!-- Label cell -->
91-
<div class="comparison-label flex items-center gap-1.5 px-4 py-3 border-b border-border">
91+
<div
92+
class="comparison-label relative bg-bg flex items-center gap-1.5 px-4 py-3 border-b border-border"
93+
>
9294
<span class="text-xs text-fg-muted uppercase tracking-wider">{{ label }}</span>
9395
<TooltipApp v-if="description" :text="description" position="top">
9496
<span class="i-lucide:info w-3 h-3 text-fg-subtle cursor-help" aria-hidden="true" />
@@ -151,3 +153,13 @@ function isCellLoading(index: number): boolean {
151153
</div>
152154
</div>
153155
</template>
156+
157+
<style lang="css" scoped>
158+
.comparison-label {
159+
position: sticky;
160+
left: 0;
161+
z-index: 2;
162+
inline-size: var(--label-column-width);
163+
min-inline-size: var(--label-column-width);
164+
}
165+
</style>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<script setup lang="ts">
2+
const { selectedPackages, selectedPackagesParam, clearSelectedPackages } = usePackageSelection()
3+
4+
const shortcutKey = 'b'
5+
const actionBar = useTemplateRef('actionBarRef')
6+
onKeyStroke(
7+
e => {
8+
const target = e.target as HTMLElement
9+
const isCheckbox = target.hasAttribute('data-package-card-checkbox')
10+
return isKeyWithoutModifiers(e, shortcutKey) && (!isEditableElement(target) || isCheckbox)
11+
},
12+
e => {
13+
if (selectedPackages.value.length === 0) {
14+
return
15+
}
16+
17+
e.preventDefault()
18+
actionBar.value?.focus()
19+
},
20+
)
21+
</script>
22+
23+
<template>
24+
<Transition name="action-bar-slide" appear>
25+
<section
26+
v-if="selectedPackages.length"
27+
aria-labelledby="action-bar-title"
28+
class="group fixed bottom-10 inset-is-0 w-full flex items-center justify-center z-36 pointer-events-none focus:outline-none"
29+
tabindex="-1"
30+
aria-keyshortcuts="b"
31+
ref="actionBarRef"
32+
>
33+
<h3 id="action-bar-title" class="sr-only">
34+
{{ $t('action_bar.title') }}
35+
</h3>
36+
<div
37+
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"
38+
>
39+
<div aria-live="polite" aria-atomic="true" class="sr-only">
40+
{{ $t('action_bar.selection', selectedPackages.length) }}.
41+
{{ $t('action_bar.shortcut', { key: shortcutKey }) }}.
42+
</div>
43+
44+
<div class="flex items-center gap-2">
45+
<span class="text-fg font-semibold text-sm flex items-center gap-1.5">
46+
{{ $t('action_bar.selection', selectedPackages.length) }}
47+
</span>
48+
<button
49+
@click="clearSelectedPackages"
50+
class="flex items-center ms-1 text-fg-muted hover:(text-fg bg-accent/10) p-1.5 rounded-lg transition-colors"
51+
:aria-label="$t('action_bar.button_close_aria_label')"
52+
>
53+
<span class="i-lucide:x text-sm" aria-hidden="true" />
54+
</button>
55+
</div>
56+
57+
<LinkBase
58+
:to="{ name: 'compare', query: { packages: selectedPackagesParam } }"
59+
variant="button-secondary"
60+
classicon="i-lucide:git-compare"
61+
>
62+
{{ $t('package.links.compare') }}
63+
</LinkBase>
64+
</div>
65+
</section>
66+
</Transition>
67+
</template>
68+
69+
<style scoped>
70+
/* Action bar slide/fade animation */
71+
.action-bar-slide-enter-active,
72+
.action-bar-slide-leave-active {
73+
transition:
74+
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
75+
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
76+
}
77+
.action-bar-slide-enter-from,
78+
.action-bar-slide-leave-to {
79+
opacity: 0;
80+
transform: translateY(40px) scale(0.98);
81+
}
82+
.action-bar-slide-enter-to,
83+
.action-bar-slide-leave-from {
84+
opacity: 1;
85+
transform: translateY(0) scale(1);
86+
}
87+
</style>

app/components/Package/Card.vue

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const props = defineProps<{
1616
searchQuery?: string
1717
}>()
1818
19+
const { isPackageSelected, togglePackageSelection, canSelectMore } = usePackageSelection()
20+
const isSelected = computed<boolean>(() => {
21+
return isPackageSelected(props.result.package.name)
22+
})
23+
1924
const emit = defineEmits<{
2025
clickKeyword: [keyword: string]
2126
}>()
@@ -39,16 +44,16 @@ const numberFormatter = useNumberFormatter()
3944
</script>
4045

4146
<template>
42-
<BaseCard :isExactMatch="isExactMatch">
43-
<div class="mb-2 flex items-baseline justify-start gap-2">
47+
<BaseCard :selected="isSelected" :isExactMatch="isExactMatch">
48+
<header class="mb-4 flex items-baseline justify-between gap-2">
4449
<component
4550
:is="headingLevel ?? 'h3'"
4651
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"
4752
>
4853
<NuxtLink
4954
:to="packageRoute(result.package.name)"
5055
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
51-
class="decoration-none scroll-mt-48 scroll-mb-6 after:content-[''] after:absolute after:inset-0"
56+
class="decoration-none after:content-[''] after:absolute after:inset-0"
5257
:data-result-index="index"
5358
dir="ltr"
5459
>{{ result.package.name }}</NuxtLink
@@ -59,28 +64,17 @@ const numberFormatter = useNumberFormatter()
5964
>{{ $t('search.exact_match') }}</span
6065
>
6166
</component>
62-
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
63-
<!-- Mobile: version next to package name -->
64-
<div class="sm:hidden text-fg-subtle flex items-center gap-1.5 shrink-0">
65-
<span
66-
v-if="result.package.version"
67-
class="font-mono text-xs truncate max-w-20"
68-
:title="result.package.version"
69-
>
70-
v{{ result.package.version }}
71-
</span>
72-
<ProvenanceBadge
73-
v-if="result.package.publisher?.trustedPublisher"
74-
:provider="result.package.publisher.trustedPublisher.id"
75-
:package-name="result.package.name"
76-
:version="result.package.version"
77-
:linked="false"
78-
compact
79-
/>
80-
</div>
81-
</div>
82-
<div class="flex justify-start items-start gap-4 sm:gap-8">
83-
<div class="min-w-0">
67+
68+
<PackageSelectionCheckbox
69+
:package-name="result.package.name"
70+
:disabled="!canSelectMore && !isSelected"
71+
:checked="isSelected"
72+
@change="togglePackageSelection"
73+
/>
74+
</header>
75+
76+
<div class="flex flex-col sm:flex-row sm:justify-start sm:items-start gap-6 sm:gap-8">
77+
<div class="min-w-0 w-full">
8478
<p v-if="pkgDescription" class="text-fg-muted text-xs sm:text-sm line-clamp-2 mb-2 sm:mb-3">
8579
<span v-html="pkgDescription" />
8680
</p>
@@ -124,10 +118,9 @@ const numberFormatter = useNumberFormatter()
124118
</div>
125119
</dl>
126120
</div>
127-
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
128-
<!-- Desktop: version and downloads on right side -->
129-
<div class="hidden sm:flex flex-col gap-2 shrink-0">
130-
<div class="text-fg-subtle flex items-start gap-2 justify-end">
121+
122+
<div class="flex flex-col gap-2 shrink-0">
123+
<div class="text-fg-subtle flex items-start gap-2 sm:justify-end">
131124
<span
132125
v-if="result.package.version"
133126
class="font-mono text-xs truncate max-w-32"
@@ -150,7 +143,7 @@ const numberFormatter = useNumberFormatter()
150143
</div>
151144
<div
152145
v-if="result.downloads?.weekly"
153-
class="text-fg-subtle gap-2 flex items-center justify-end"
146+
class="text-fg-subtle gap-2 flex items-center sm:justify-end"
154147
>
155148
<span class="i-lucide:chart-line w-3.5 h-3.5" aria-hidden="true" />
156149
<span class="font-mono text-xs">

0 commit comments

Comments
 (0)