+
+
+
diff --git a/app/components/Package/List.vue b/app/components/Package/List.vue
index 92fa300c0..c7bc5f885 100644
--- a/app/components/Package/List.vue
+++ b/app/components/Package/List.vue
@@ -179,7 +179,8 @@ defineExpose({
('pageSize', { required: true })
const emit = defineEmits<{
'toggleColumn': [columnId: ColumnId]
+ 'toggleSelection': []
'resetColumns': []
'clearFilter': [chip: FilterChip]
'clearAllFilters': []
@@ -110,6 +111,8 @@ const sortKeyLabelKeys = computed>(() => ({
function getSortKeyLabelKey(key: SortKey): string {
return sortKeyLabelKeys.value[key]
}
+
+const { selectedPackages, clearSelectedPackages } = usePackageSelection()
@@ -211,6 +214,22 @@ function getSortKeyLabelKey(key: SortKey): string {
+
+
+
+ {{ t('filters.view_selected') }} ({{ selectedPackages.length }})
+
+
+
diff --git a/app/components/Package/SelectionView.vue b/app/components/Package/SelectionView.vue
new file mode 100644
index 000000000..6b122f390
--- /dev/null
+++ b/app/components/Package/SelectionView.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ {{ $t('action_bar.selection', selectedPackages.length) }}
+
+
+
+
+
diff --git a/app/components/Package/Table.vue b/app/components/Package/Table.vue
index 63ec2f70f..3f7e93d45 100644
--- a/app/components/Package/Table.vue
+++ b/app/components/Package/Table.vue
@@ -102,6 +102,7 @@ const columnLabels = computed(() => ({
maintenanceScore: t('filters.columns.maintenance_score'),
combinedScore: t('filters.columns.combined_score'),
security: t('filters.columns.security'),
+ selection: t('filters.columns.selection'),
}))
function getColumnLabel(id: ColumnId): string {
@@ -299,6 +300,9 @@ function getColumnLabel(id: ColumnId): string {
>
{{ getColumnLabel('security') }}
+
+ {{ getColumnLabel('selection') }}
+ |
diff --git a/app/components/Package/TableRow.vue b/app/components/Package/TableRow.vue
index 267b7f1b9..ccd709bf0 100644
--- a/app/components/Package/TableRow.vue
+++ b/app/components/Package/TableRow.vue
@@ -17,6 +17,10 @@ const pkg = computed(() => props.result.package)
const score = computed(() => props.result.score)
const updatedDate = computed(() => props.result.package.date)
+const { isPackageSelected, togglePackageSelection } = usePackageSelection()
+const isSelected = computed(() => {
+ return isPackageSelected(props.result)
+})
function formatDownloads(count?: number): string {
if (count === undefined) return '-'
@@ -196,6 +200,23 @@ const allMaintainersText = computed(() => {
-
+
+
+
+
+
+ |
diff --git a/app/composables/usePackageSelection.ts b/app/composables/usePackageSelection.ts
new file mode 100644
index 000000000..eeef29b08
--- /dev/null
+++ b/app/composables/usePackageSelection.ts
@@ -0,0 +1,32 @@
+export function usePackageSelection() {
+ const selectedPackages = useState('package_selection', () => [])
+ const selectedPackagesParam = computed(() =>
+ selectedPackages.value.map(p => p.package.name).join(','),
+ )
+
+ function isPackageSelected(pkg: NpmSearchResult): boolean {
+ return selectedPackages.value.some(p => p.package.name === pkg.package.name)
+ }
+
+ function togglePackageSelection(pkg: NpmSearchResult) {
+ if (isPackageSelected(pkg)) {
+ selectedPackages.value = selectedPackages.value.filter(
+ selected => selected.package.name !== pkg.package.name,
+ )
+ } else {
+ selectedPackages.value = [...selectedPackages.value, pkg]
+ }
+ }
+
+ function clearSelectedPackages() {
+ selectedPackages.value = []
+ }
+
+ return {
+ selectedPackages,
+ selectedPackagesParam,
+ clearSelectedPackages,
+ isPackageSelected,
+ togglePackageSelection,
+ }
+}
diff --git a/app/pages/search.vue b/app/pages/search.vue
index e71faaf25..6ed60ad48 100644
--- a/app/pages/search.vue
+++ b/app/pages/search.vue
@@ -9,6 +9,22 @@ import { normalizeSearchParam } from '#shared/utils/url'
const route = useRoute()
+const { selectedPackages } = usePackageSelection()
+const isSelectioView = ref(false)
+
+watch(selectedPackages, packages => {
+ if (packages.length === 0) {
+ isSelectioView.value = false
+ }
+})
+
+function showSelectionView() {
+ isSelectioView.value = true
+}
+function hideSelectionView() {
+ isSelectioView.value = false
+}
+
// Preferences (persisted to localStorage)
const {
viewMode,
@@ -543,16 +559,33 @@ defineOgImageComponent('Default', {
+
+
{{ $t('search.title') }}
-
+
+
-
+
+
+
{
expect(results.violations).toEqual([])
})
})
+
+ describe('PackageActionBar', () => {
+ it('should have no accessibility violations', async () => {
+ const component = await mountSuspended(PackageActionBar)
+ const results = await runAxe(component)
+ expect(results.violations).toEqual([])
+ })
+ })
+
+ describe('PackageSelectionView', () => {
+ it('should have no accessibility violations', async () => {
+ const component = await mountSuspended(PackageSelectionView)
+ const results = await runAxe(component)
+ expect(results.violations).toEqual([])
+ })
+
+ it('should have no accessibility violations changing view mode', async () => {
+ const component = await mountSuspended(PackageSelectionView, {
+ props: {
+ viewMode: 'table',
+ },
+ })
+ const results = await runAxe(component)
+ expect(results.violations).toEqual([])
+ })
+ })
})
function applyTheme(colorMode: string, bgTheme: string | null) {