Skip to content

Commit ed13532

Browse files
feat: add packages in route query
1 parent 9c8a88c commit ed13532

7 files changed

Lines changed: 90 additions & 41 deletions

File tree

app/components/Package/ActionBar.vue

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script setup lang="ts">
2-
const { selectedPackages, clearSelectedPackages } = usePackageSelection()
3-
const comparePackagesQuery = computed<string>(() =>
4-
selectedPackages.value.map(pkg => pkg.package.name).join(','),
5-
)
2+
defineProps<{
3+
hide?: boolean
4+
}>()
5+
6+
const { selectedPackages, selectedPackagesParam, clearSelectedPackages } = usePackageSelection()
67
78
const shortcutKey = 'b'
89
const actionBar = useTemplateRef('actionBarRef')
@@ -22,7 +23,7 @@ onKeyStroke(
2223
<template>
2324
<Transition name="action-bar-slide" appear>
2425
<div
25-
v-if="selectedPackages.length"
26+
v-if="selectedPackages.length && !hide"
2627
class="fixed bottom-12 inset-is-0 w-full flex items-center justify-center z-36 pointer-events-none"
2728
>
2829
<div
@@ -49,9 +50,10 @@ onKeyStroke(
4950

5051
<div>
5152
<LinkBase
52-
:to="{ name: 'compare', query: { packages: comparePackagesQuery } }"
53+
:to="{ name: 'compare', query: { packages: selectedPackagesParam } }"
5354
variant="button-secondary"
5455
classicon="i-lucide:git-compare"
56+
:disabled="selectedPackages.length > 4"
5557
>
5658
Compare
5759
</LinkBase>

app/components/Package/Card.vue

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ const props = defineProps<{
1414
filters?: StructuredFilters
1515
/** Search query for highlighting exact matches */
1616
searchQuery?: string
17-
/** Shows checkbox to all cards and the click will work as selection */
18-
forceSelection?: boolean
1917
}>()
2018
2119
const selected = defineModel<boolean>('selected', {
@@ -51,20 +49,10 @@ const numberFormatter = useNumberFormatter()
5149
:is="headingLevel ?? 'h3'"
5250
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"
5351
>
54-
<button
55-
v-if="forceSelection"
56-
@click="selected = !selected"
57-
class="after:content-[''] after:absolute after:inset-0 cursor-pointer"
58-
:data-result-index="index"
59-
dir="ltr"
60-
>
61-
{{ result.package.name }}
62-
</button>
6352
<NuxtLink
64-
v-else
6553
:to="packageRoute(result.package.name)"
6654
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
67-
class="decoration-none scroll-mt-48 scroll-mb-6 after:content-[''] after:absolute after:inset-0"
55+
class="decoration-none after:content-[''] after:absolute after:inset-0"
6856
:data-result-index="index"
6957
dir="ltr"
7058
>{{ result.package.name }}</NuxtLink
@@ -81,7 +69,6 @@ const numberFormatter = useNumberFormatter()
8169
<input
8270
data-package-card-checkbox
8371
class="md:opacity-0 group-focus-within:opacity-100 checked:opacity-100 md:group-hover:opacity-100 size-4 cursor-pointer accent-accent border border-fg-muted/30 hover:border-accent transition-colors"
84-
:class="{ 'opacity-100!': forceSelection }"
8572
type="checkbox"
8673
:checked="!!selected"
8774
v-model="selected"

app/components/Package/List.vue

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ defineExpose({
148148
scrollToIndex,
149149
})
150150
151-
const { selectedPackages, isPackageSelected, togglePackageSelection } = usePackageSelection()
151+
const { isPackageSelected, togglePackageSelection } = usePackageSelection()
152152
</script>
153153

154154
<template>
@@ -189,9 +189,8 @@ const { selectedPackages, isPackageSelected, togglePackageSelection } = usePacka
189189
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
190190
:filters="filters"
191191
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
192-
:selected="isPackageSelected(item)"
193-
:force-selection="selectedPackages.length > 0"
194-
@update:selected="togglePackageSelection(item)"
192+
:selected="isPackageSelected(item.package.name)"
193+
@update:selected="togglePackageSelection(item.package.name)"
195194
@click-keyword="emit('clickKeyword', $event)"
196195
/>
197196
</div>

app/components/Package/ListToolbar.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,13 @@ const { selectedPackages, clearSelectedPackages } = usePackageSelection()
235235
class="flex items-center order-3 border-is border-fg-subtle/20 ps-3"
236236
v-if="selectedPackages.length"
237237
>
238-
<LinkBase
239-
to="/package-selection"
240-
variant="button-secondary"
238+
<ButtonBase
239+
variant="secondary"
240+
@click="emit('toggleSelection')"
241241
classicon="i-lucide:package-check"
242242
>
243243
View selected ({{ selectedPackages.length }})
244-
</LinkBase>
244+
</ButtonBase>
245245
<button @click="clearSelectedPackages" class="flex items-center ms-2">
246246
<span class="i-lucide:x text-sm" aria-label="Close action bar" />
247247
</button>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
viewMode?: ViewMode
4+
}>()
5+
6+
const { selectedPackages } = usePackageSelection()
7+
const packages = ref<NpmSearchResult[]>([])
8+
</script>
9+
10+
<template>
11+
<section>
12+
<p class="text-fg-muted text-sm mt-4 font-mono">
13+
{{
14+
$t('search.found_packages', { count: $n(selectedPackages.length) }, selectedPackages.length)
15+
}}
16+
</p>
17+
18+
{{ selectedPackages }}
19+
20+
<div class="mt-6">
21+
<PackageList
22+
v-if="packages.length > 0"
23+
:view-mode="viewMode"
24+
:results="packages"
25+
search-context
26+
heading-level="h2"
27+
show-publisher
28+
/>
29+
</div>
30+
</section>
31+
</template>

app/composables/usePackageSelection.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
11
export function usePackageSelection() {
2-
const selectedPackages = useState<NpmSearchResult[]>('selected_packages', () => [])
2+
const selectedPackagesParam = useRouteQuery<string>('selection', '', {
3+
mode: 'replace',
4+
})
35

4-
function findPackageIndex(pkg: NpmSearchResult): number {
5-
return selectedPackages.value.findIndex(
6-
selectedPackage => selectedPackage.package.name === pkg.package.name,
7-
)
6+
const selectedPackages = computed<string[]>({
7+
get() {
8+
if (!selectedPackagesParam) {
9+
return []
10+
}
11+
12+
return selectedPackagesParam.value.split(',').map(p => p.trim())
13+
},
14+
set(value) {
15+
selectedPackagesParam.value = value.length > 0 ? value.join(',') : ''
16+
},
17+
})
18+
19+
function findPackageIndex(name: string): number {
20+
return selectedPackages.value.findIndex(selectedPackage => selectedPackage === name)
821
}
922

10-
function isPackageSelected(pkg: NpmSearchResult): boolean {
11-
return findPackageIndex(pkg) !== -1
23+
function isPackageSelected(name: string): boolean {
24+
return findPackageIndex(name) !== -1
1225
}
1326

14-
function togglePackageSelection(pkg: NpmSearchResult) {
15-
const itemIndex = findPackageIndex(pkg)
27+
function togglePackageSelection(name: string) {
28+
const itemIndex = findPackageIndex(name)
1629

1730
if (itemIndex !== -1) {
18-
selectedPackages.value.splice(itemIndex, 1)
31+
selectedPackages.value = selectedPackages.value.filter((_, i) => i !== itemIndex)
1932
} else {
20-
selectedPackages.value.push(pkg)
33+
selectedPackages.value = [...selectedPackages.value, name]
2134
}
2235
}
2336

@@ -27,6 +40,7 @@ export function usePackageSelection() {
2740

2841
return {
2942
selectedPackages,
43+
selectedPackagesParam,
3044
clearSelectedPackages,
3145
isPackageSelected,
3246
togglePackageSelection,

app/pages/search.vue

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ import { normalizeSearchParam } from '#shared/utils/url'
1010
const route = useRoute()
1111
const router = useRouter()
1212
13+
const { selectedPackages } = usePackageSelection()
14+
const selectionView = useRouteQuery<string>('selection-view', '', { mode: 'push' })
15+
16+
function toggleSelection() {
17+
selectionView.value = selectionView.value === 'true' ? '' : 'true'
18+
}
19+
20+
const isSelectioView = computed<boolean>(() => selectionView.value === 'true')
21+
1322
// Preferences (persisted to localStorage)
1423
const {
1524
viewMode,
@@ -533,7 +542,8 @@ defineOgImageComponent('Default', {
533542
</script>
534543

535544
<template>
536-
<PackageActionBar />
545+
<PackageActionBar :hide="isSelectioView" />
546+
537547
<main class="flex-1 py-8" :class="{ 'overflow-x-hidden': viewMode !== 'table' }">
538548
<div class="container-sm">
539549
<div class="flex items-center justify-between gap-4 mb-4">
@@ -543,7 +553,12 @@ defineOgImageComponent('Default', {
543553
<SearchProviderToggle />
544554
</div>
545555

546-
<section v-if="query">
556+
<PackageSelectionView
557+
:view-mode="viewMode"
558+
v-if="isSelectioView && selectedPackages.length"
559+
/>
560+
561+
<section v-else-if="query">
547562
<!-- Initial loading (only after user interaction, not during view transition) -->
548563
<LoadingSpinner v-if="showSearching" :text="$t('search.searching')" />
549564

@@ -606,6 +621,7 @@ defineOgImageComponent('Default', {
606621
:disabled-sort-keys="disabledSortKeys"
607622
search-context
608623
@toggle-column="toggleColumn"
624+
@toggle-selection="toggleSelection"
609625
@reset-columns="resetColumns"
610626
@clear-filter="handleClearFilter"
611627
@clear-all-filters="clearAllFilters"

0 commit comments

Comments
 (0)