Skip to content

feat(ui): add multi-select and bulk actions for packages#1672

Open
MatteoGabriele wants to merge 41 commits intonpmx-dev:mainfrom
MatteoGabriele:feat/action-bar
Open

feat(ui): add multi-select and bulk actions for packages#1672
MatteoGabriele wants to merge 41 commits intonpmx-dev:mainfrom
MatteoGabriele:feat/action-bar

Conversation

@MatteoGabriele
Copy link
Contributor

@MatteoGabriele MatteoGabriele commented Feb 26, 2026

🔗 Linked issue

resolves #1509

🧭 Context

Added a multi-select feature to the search page that allows users to select multiple packages and perform bulk actions on them. Currently supports comparing selected packages, with the possibility of adding more actions in the future.

📚 Description

  • Selection UI: Added checkboxes to package cards that appear on hover. Selected cards are visually indicated with a border highlight and checkbox state.
  • Persistent counter: New "View selected (X)" button in the toolbar shows active selections and navigates to a dedicated view for managing them.
  • Floating action bar: When items are selected, a floating action bar appears at the bottom with the selection count, primary action (Compare), and clear button.
  • Selection state management: Uses composable to maintain selections across view changes (card/table/selections view), allowing users to continue browsing while keeping their selections.
  • Selection view: It's a separate component view that retrieves each saved component similar to the Compare page request logic.
  • Accessibility: Includes aria-live announcements for selection changes and keyboard shortcuts ("b" key) to focus the action bar.

At the moment, the logic is locked at a maximum of 4 selectable items. This won't scale, but for now, to reduce complexity, it will mimic what's needed by the Compare page, which is the only current functionality available in the multi-select.
My take is to re-think this along the way, when and if another action gets added.

Screen.Recording.2026-02-27.at.18.33.58.mov

@vercel
Copy link

vercel bot commented Feb 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs.npmx.dev Ready Ready Preview, Comment Mar 4, 2026 0:42am
npmx.dev Ready Ready Preview, Comment Mar 4, 2026 0:42am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Mar 4, 2026 0:42am

Request Review

@github-actions
Copy link

github-actions bot commented Feb 26, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
i18n/locales/it-IT.json Localization changed, will be marked as complete. 🔄️
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@codecov
Copy link

codecov bot commented Feb 27, 2026

Codecov Report

❌ Patch coverage is 62.28070% with 43 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/usePackageSelection.ts 34.21% 19 Missing and 6 partials ⚠️
app/components/Package/ActionBar.vue 45.00% 8 Missing and 3 partials ⚠️
app/components/Package/ListToolbar.vue 50.00% 3 Missing and 1 partial ⚠️
app/router.options.ts 66.66% 2 Missing ⚠️
app/components/Package/TableRow.vue 83.33% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@MatteoGabriele
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
app/composables/usePackageSelection.ts (1)

10-22: Canonicalise selection values before persisting to query state.

selectedPackages reads a capped list, but Line 20-Line 21 can still write duplicates or over-limit values into the URL. Keeping read/write logic symmetrical avoids query-state drift.

♻️ Suggested normalisation patch
 const selectedPackages = computed<string[]>({
   get() {
     const raw = selectedPackagesParam.value
     if (!raw) return []
-    return raw
-      .split(',')
-      .map(p => String(p).trim())
-      .filter(Boolean)
-      .slice(0, MAX_PACKAGE_SELECTION)
+    return [...new Set(
+      raw
+        .split(',')
+        .map(p => String(p).trim())
+        .filter(Boolean),
+    )].slice(0, MAX_PACKAGE_SELECTION)
   },
   set(pkgs: string[]) {
-    // Ensure all items are strings before joining
-    const validPkgs = (Array.isArray(pkgs) ? pkgs : []).map(p => String(p).trim()).filter(Boolean)
+    const validPkgs = [...new Set(
+      (Array.isArray(pkgs) ? pkgs : [])
+        .map(p => String(p).trim())
+        .filter(Boolean),
+    )].slice(0, MAX_PACKAGE_SELECTION)
     selectedPackagesParam.value = validPkgs.length > 0 ? validPkgs.join(',') : ''
   },
 })

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3856538 and 70f7615.

📒 Files selected for processing (17)
  • app/components/Package/ActionBar.vue
  • app/components/Package/Card.vue
  • app/components/Package/ListToolbar.vue
  • app/components/Package/SelectionView.vue
  • app/components/Package/Table.vue
  • app/components/Package/TableRow.vue
  • app/composables/usePackageSelection.ts
  • app/pages/search.vue
  • app/router.options.ts
  • i18n/locales/en.json
  • i18n/locales/it-IT.json
  • i18n/schema.json
  • lunaria/files/en-GB.json
  • lunaria/files/en-US.json
  • lunaria/files/it-IT.json
  • shared/types/npm-registry.ts
  • test/unit/shared/types/index.spec.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • lunaria/files/en-US.json
  • i18n/locales/en.json
  • app/components/Package/ActionBar.vue
  • app/components/Package/SelectionView.vue
  • app/components/Package/ListToolbar.vue
  • app/components/Package/Table.vue
  • i18n/schema.json

after merge conflict, the v-else-if statement got removed by accident
Copy link
Member

@knowler knowler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a partial review. Thank you for considering the accessibility of this feature!

Comment on lines +204 to +211
<td class="pe-2">
<PackageSelectionCheckbox
:package-name="result.package.name"
:disabled="isMaxSelected && !isSelected"
:checked="isSelected"
@change="togglePackageSelection"
/>
</td>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider moving this to the first column. It’s a lot of work for some users to trace from the start to the end of a table row to match selection with a package name.

Comment on lines +29 to +34
<div
ref="actionBarRef"
tabindex="-1"
aria-keyshortcuts="b"
class="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"
>
Copy link
Member

@knowler knowler Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this container is focusable (even if programmatically) and it currently is not within a landmark (best practice is to put everything in a landmark), I’m thinking that we should make this a region landmark. We can do so with a named <section> element (either using aria-label or aria-labelledby), since when named that is implicitly a region landmark. Since single character keys often conflict with screen reader shortcuts, this provides another means of navigating to the bar.

>
<div aria-live="polite" aria-atomic="true" class="sr-only">
{{ $t('action_bar.selection', selectedPackages.length) }}.
{{ $t('action_bar.shortcut', { key: shortcutKey }) }}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that including the search shortcut message each time could get annoying for screen reader users. It is at least at the end. I might need to think more on this.

@MatteoGabriele
Copy link
Contributor Author

Thanks a lot for the feedback @knowler
The accessibility part was difficult for me to understand because, at least for the cards view, the action bar is pretty much unreachable.

Open to making changes for better accessibility. In the meantime, I will apply some of your suggestions and see where we land.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ux Related to wider UX decisions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Select packages to compare from the search results view

3 participants