-
-
Notifications
You must be signed in to change notification settings - Fork 283
feat(ui): add action bar #1672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(ui): add action bar #1672
Changes from all commits
a880463
3a12c63
9c8a88c
ed13532
812f685
3d244a0
287a4a7
8743c79
7700125
1ee8c67
bab24a2
a96417e
481fc12
5e926ab
d04f0d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,79 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { selectedPackages, selectedPackagesParam, clearSelectedPackages } = usePackageSelection() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const shortcutKey = 'b' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const actionBar = useTemplateRef('actionBarRef') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onKeyStroke( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const target = e.target as HTMLElement | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isCheckbox = target.hasAttribute('data-package-card-checkbox') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return isKeyWithoutModifiers(e, shortcutKey) && (!isEditableElement(target) || isCheckbox) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (selectedPackages.value.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actionBar.value?.focus() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Transition name="action-bar-slide" appear> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| v-if="selectedPackages.length" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="fixed bottom-10 inset-is-0 w-full flex items-center justify-center z-36 pointer-events-none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref="actionBarRef" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tabindex="-1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-keyshortcuts="b" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="pointer-events-auto bg-bg shadow-xl shadow-accent/5 border border-fg-muted/20 p-2.5 min-w-[300px] rounded-xl flex gap-2 items-center justify-between animate-in" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div aria-live="polite" aria-atomic="true" class="sr-only"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ $t('action_bar.selection', selectedPackages.length) }}. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ $t('action_bar.shortcut', { key: shortcutKey }) }}. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="flex items-center gap-1 ms-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="text-fg text-sm"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ $t('action_bar.selection', selectedPackages.length) }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button @click="clearSelectedPackages" class="flex items-center ms-2 hover:text-fg-muted"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="i-lucide:x text-xs relative top-px" aria-label="Close action bar" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <LinkBase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :to="{ name: 'compare', query: { packages: selectedPackagesParam } }" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="button-secondary" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| classicon="i-lucide:git-compare" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Compare | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </LinkBase> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The action-bar controls need accessible labelling and localisation cleanup. Line 40-42 uses an icon-only close button without a button-level label, and Line 50 hardcodes visible text ( ✅ Suggested patch- <button `@click`="clearSelectedPackages" class="flex items-center ms-2 hover:text-fg-muted">
- <span class="i-lucide:x text-xs relative top-px" aria-label="Close action bar" />
+ <button
+ type="button"
+ :aria-label="$t('common.close')"
+ `@click`="clearSelectedPackages"
+ class="flex items-center ms-2 hover:text-fg-muted"
+ >
+ <span class="i-lucide:x text-xs relative top-px" aria-hidden="true" />
</button>
@@
- Compare
+ {{ $t('package.links.compare') }}
</LinkBase>📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Transition> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <style scoped> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Action bar slide/fade animation */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .action-bar-slide-enter-active, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .action-bar-slide-leave-active { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transition: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .action-bar-slide-enter-from, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .action-bar-slide-leave-to { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform: translateY(40px) scale(0.98); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .action-bar-slide-enter-to, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .action-bar-slide-leave-from { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform: translateY(0) scale(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </style> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,11 @@ const props = defineProps<{ | |
| searchQuery?: string | ||
| }>() | ||
|
|
||
| const { isPackageSelected, togglePackageSelection } = usePackageSelection() | ||
| const isSelected = computed<boolean>(() => { | ||
| return isPackageSelected(props.result) | ||
| }) | ||
|
|
||
| const emit = defineEmits<{ | ||
| clickKeyword: [keyword: string] | ||
| }>() | ||
|
|
@@ -39,16 +44,16 @@ const numberFormatter = useNumberFormatter() | |
| </script> | ||
|
|
||
| <template> | ||
| <BaseCard :isExactMatch="isExactMatch"> | ||
| <div class="mb-2 flex items-baseline justify-start gap-2"> | ||
| <BaseCard :selected="isSelected" :isExactMatch="isExactMatch"> | ||
| <header class="mb-4 flex items-baseline justify-between gap-2"> | ||
| <component | ||
| :is="headingLevel ?? 'h3'" | ||
| 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" | ||
| > | ||
| <NuxtLink | ||
| :to="packageRoute(result.package.name)" | ||
| :prefetch-on="prefetch ? 'visibility' : 'interaction'" | ||
| class="decoration-none scroll-mt-48 scroll-mb-6 after:content-[''] after:absolute after:inset-0" | ||
| class="decoration-none after:content-[''] after:absolute after:inset-0" | ||
| :data-result-index="index" | ||
| dir="ltr" | ||
| >{{ result.package.name }}</NuxtLink | ||
|
|
@@ -59,28 +64,25 @@ const numberFormatter = useNumberFormatter() | |
| >{{ $t('search.exact_match') }}</span | ||
| > | ||
| </component> | ||
| <span aria-hidden="true" class="flex-shrink-1 flex-grow-1" /> | ||
| <!-- Mobile: version next to package name --> | ||
| <div class="sm:hidden text-fg-subtle flex items-center gap-1.5 shrink-0"> | ||
| <span | ||
| v-if="result.package.version" | ||
| class="font-mono text-xs truncate max-w-20" | ||
| :title="result.package.version" | ||
| > | ||
| v{{ result.package.version }} | ||
| </span> | ||
| <ProvenanceBadge | ||
| v-if="result.package.publisher?.trustedPublisher" | ||
| :provider="result.package.publisher.trustedPublisher.id" | ||
| :package-name="result.package.name" | ||
| :version="result.package.version" | ||
| :linked="false" | ||
| compact | ||
| /> | ||
|
|
||
| <div class="relative z-1"> | ||
| <label> | ||
| <span class="sr-only"> | ||
| {{ $t('package.card.select') }} | ||
| </span> | ||
|
Comment on lines
+70
to
+72
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make checkbox accessible names package-specific. Each checkbox currently announces the same label (“Select package”), which is ambiguous when navigating multiple results with assistive tech. Include the package name in the accessible label. 💡 Suggested tweak- <span class="sr-only">
- {{ $t('package.card.select') }}
- </span>
+ <span class="sr-only">
+ {{ `${$t('package.card.select')}: ${result.package.name}` }}
+ </span> |
||
| <input | ||
| data-package-card-checkbox | ||
| 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" | ||
| type="checkbox" | ||
| :checked="isSelected" | ||
| @change="togglePackageSelection(result)" | ||
| /> | ||
| </label> | ||
| </div> | ||
| </div> | ||
| <div class="flex justify-start items-start gap-4 sm:gap-8"> | ||
| <div class="min-w-0"> | ||
| </header> | ||
|
|
||
| <div class="flex flex-col sm:flex-row sm:justify-start sm:items-start gap-6 sm:gap-8"> | ||
| <div class="min-w-0 w-full"> | ||
| <p v-if="pkgDescription" class="text-fg-muted text-xs sm:text-sm line-clamp-2 mb-2 sm:mb-3"> | ||
| <span v-html="pkgDescription" /> | ||
| </p> | ||
|
|
@@ -124,10 +126,9 @@ const numberFormatter = useNumberFormatter() | |
| </div> | ||
| </dl> | ||
| </div> | ||
| <span aria-hidden="true" class="flex-shrink-1 flex-grow-1" /> | ||
| <!-- Desktop: version and downloads on right side --> | ||
| <div class="hidden sm:flex flex-col gap-2 shrink-0"> | ||
| <div class="text-fg-subtle flex items-start gap-2 justify-end"> | ||
|
|
||
| <div class="flex flex-col gap-2 shrink-0"> | ||
| <div class="text-fg-subtle flex items-start gap-2 sm:justify-end"> | ||
| <span | ||
| v-if="result.package.version" | ||
| class="font-mono text-xs truncate max-w-32" | ||
|
|
@@ -150,7 +151,7 @@ const numberFormatter = useNumberFormatter() | |
| </div> | ||
| <div | ||
| v-if="result.downloads?.weekly" | ||
| class="text-fg-subtle gap-2 flex items-center justify-end" | ||
| class="text-fg-subtle gap-2 flex items-center sm:justify-end" | ||
| > | ||
| <span class="i-lucide:chart-line w-3.5 h-3.5" aria-hidden="true" /> | ||
| <span class="font-mono text-xs"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -43,6 +43,7 @@ const pageSize = defineModel<PageSize>('pageSize', { required: true }) | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const emit = defineEmits<{ | ||||||||||||||||||||||||
| 'toggleColumn': [columnId: ColumnId] | ||||||||||||||||||||||||
| 'toggleSelection': [] | ||||||||||||||||||||||||
| 'resetColumns': [] | ||||||||||||||||||||||||
| 'clearFilter': [chip: FilterChip] | ||||||||||||||||||||||||
| 'clearAllFilters': [] | ||||||||||||||||||||||||
|
|
@@ -110,6 +111,8 @@ const sortKeyLabelKeys = computed<Record<SortKey, string>>(() => ({ | |||||||||||||||||||||||
| function getSortKeyLabelKey(key: SortKey): string { | ||||||||||||||||||||||||
| return sortKeyLabelKeys.value[key] | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const { selectedPackages, clearSelectedPackages } = usePackageSelection() | ||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||
|
|
@@ -211,6 +214,22 @@ function getSortKeyLabelKey(key: SortKey): string { | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <ViewModeToggle v-model="viewMode" /> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||
| class="flex items-center order-3 border-is border-fg-subtle/20 ps-3" | ||||||||||||||||||||||||
| v-if="selectedPackages.length" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| <ButtonBase | ||||||||||||||||||||||||
| variant="secondary" | ||||||||||||||||||||||||
| @click="emit('toggleSelection')" | ||||||||||||||||||||||||
| classicon="i-lucide:package-check" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| {{ t('filters.view_selected') }} ({{ selectedPackages.length }}) | ||||||||||||||||||||||||
| </ButtonBase> | ||||||||||||||||||||||||
| <button @click="clearSelectedPackages" class="flex items-center ms-2"> | ||||||||||||||||||||||||
| <span class="i-lucide:x text-sm" aria-label="Close action bar" /> | ||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||
|
Comment on lines
+229
to
+231
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clear-selection icon button is missing a reliable accessible name. Line 229 renders an icon-only button, but the label is on the child ✅ Suggested patch- <button `@click`="clearSelectedPackages" class="flex items-center ms-2">
- <span class="i-lucide:x text-sm" aria-label="Close action bar" />
+ <button
+ type="button"
+ :aria-label="t('common.close')"
+ `@click`="clearSelectedPackages"
+ class="flex items-center ms-2"
+ >
+ <span class="i-lucide:x text-sm" aria-hidden="true" />
</button>📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||||||
| defineProps<{ | ||||||||||||||||||||
| viewMode?: ViewMode | ||||||||||||||||||||
| }>() | ||||||||||||||||||||
|
Comment on lines
+2
to
+4
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Verify ViewMode import status in the target file and check for global declarations
rg -n "defineProps<\{|viewMode\?: ViewMode|import.*ViewMode|export.*ViewMode" app/components/Package/SelectionView.vue
# Check for global ViewMode declarations
rg -n "type ViewMode|interface ViewMode|declare.*ViewMode" --type ts --type d.tsRepository: npmx-dev/npmx.dev Length of output: 146 🏁 Script executed: # Read the full SelectionView.vue file to see all imports
cat -n app/components/Package/SelectionView.vue
# Search for ViewMode type definition globally with proper patterns
rg "type ViewMode|interface ViewMode" --type ts
# Check if the suggested import path exists
fd "preferences" --type fRepository: npmx-dev/npmx.dev Length of output: 1753 🏁 Script executed: # Search for TypeScript/Nuxt configuration to verify the `#shared` alias
fd "tsconfig\|nuxt\.config\|vite\.config" --type f
# Check for any global type declarations
rg "declare global|declare module|ambient" --type ts
# Search for any .d.ts files at root or shared directories
find . -name "*.d.ts" -type f | head -20Repository: npmx-dev/npmx.dev Length of output: 514 🏁 Script executed: # Find and inspect TypeScript/Nuxt config files
cat -n tsconfig.json 2>/dev/null || echo "tsconfig.json not found"
cat -n nuxt.config.ts 2>/dev/null || echo "nuxt.config.ts not found"
# Also check for alias resolution in vite config or other config
find . -maxdepth 2 -name "nuxt.config.*" -o -name "tsconfig.json" | head -5Repository: npmx-dev/npmx.dev Length of output: 13729 🏁 Script executed: # Check if Nuxt auto-generates `#shared` alias by looking for nuxt auto-import config
# or check if there's any auto-import configuration for types
rg "auto|#shared" nuxt.config.ts
# Verify if ViewMode is used elsewhere and how it's imported
rg "ViewMode" --type ts --type vue | head -30Repository: npmx-dev/npmx.dev Length of output: 145 🏁 Script executed: # Check how ViewMode is imported in other files
rg "import.*ViewMode" --type ts
# Also check the app's tsconfig to see if it has the `#shared` alias
cat -n .nuxt/tsconfig.app.json 2>/dev/null | head -50Repository: npmx-dev/npmx.dev Length of output: 43 🏁 Script executed: # Check all files in .nuxt directory that might have path aliases
find .nuxt -name "tsconfig*.json" -type f | head -10
# Check if any tsconfig actually has the `#shared` path
rg "#shared" --type ts
# Let's look for any import of ViewMode in the entire codebase
rg "ViewMode" | grep -i importRepository: npmx-dev/npmx.dev Length of output: 18137
The <script setup lang="ts">
+import type { ViewMode } from '#shared/types/preferences'
+
defineProps<{
viewMode?: ViewMode
}>()📝 Committable suggestion
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| const { selectedPackages, clearSelectedPackages, selectedPackagesParam } = usePackageSelection() | ||||||||||||||||||||
| </script> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| <template> | ||||||||||||||||||||
| <section> | ||||||||||||||||||||
| <header class="mb-6 flex items-center justify-end"> | ||||||||||||||||||||
| <div class="flex items-center gap-2"> | ||||||||||||||||||||
| <ButtonBase variant="secondary" @click="clearSelectedPackages" classicon="i-lucide:x"> | ||||||||||||||||||||
| {{ $t('filters.clear_all') }} | ||||||||||||||||||||
| </ButtonBase> | ||||||||||||||||||||
| <LinkBase | ||||||||||||||||||||
| :to="{ name: 'compare', query: { packages: selectedPackagesParam } }" | ||||||||||||||||||||
| variant="button-primary" | ||||||||||||||||||||
| classicon="i-lucide:git-compare" | ||||||||||||||||||||
| > | ||||||||||||||||||||
| {{ $t('package.links.compare') }} | ||||||||||||||||||||
| </LinkBase> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| </header> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| <p class="text-fg-muted text-sm font-mono"> | ||||||||||||||||||||
| {{ $t('action_bar.selection', selectedPackages.length) }} | ||||||||||||||||||||
| </p> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| <div class="mt-6"> | ||||||||||||||||||||
| <PackageList | ||||||||||||||||||||
| v-if="selectedPackages.length > 0" | ||||||||||||||||||||
| :view-mode="viewMode" | ||||||||||||||||||||
| :results="selectedPackages" | ||||||||||||||||||||
| search-context | ||||||||||||||||||||
| heading-level="h2" | ||||||||||||||||||||
| show-publisher | ||||||||||||||||||||
| /> | ||||||||||||||||||||
| </div> | ||||||||||||||||||||
| </section> | ||||||||||||||||||||
| </template> | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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') }} | ||
| </th> | ||
| <th class="w-8"> | ||
| <span class="sr-only">{{ getColumnLabel('selection') }}</span> | ||
| </th> | ||
|
Comment on lines
+303
to
+305
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Selection header introduces a loading-state column mismatch. After adding this 💡 Suggested patch- <th class="w-8">
+ <th scope="col" class="w-8">
<span class="sr-only">{{ getColumnLabel('selection') }}</span>
</th> <template v-if="isLoading && results.length === 0">
<tr v-for="i in 5" :key="`skeleton-${i}`" class="border-b border-border">
@@
<td v-if="isColumnVisible('keywords')" class="py-3 px-3">
<div class="h-4 w-32 bg-bg-muted rounded animate-pulse ms-auto" />
</td>
+ <td class="py-3 px-3 w-8">
+ <div class="h-4 w-4 bg-bg-muted rounded animate-pulse ms-auto" />
+ </td>
</tr>
</template> |
||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| export function usePackageSelection() { | ||
| const selectedPackages = useState<NpmSearchResult[]>('package_selection', () => []) | ||
| const selectedPackagesParam = computed<string>(() => | ||
| 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, | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.