Skip to content

Commit 1b38d5b

Browse files
committed
Merge remote-tracking branch 'origin/main' into refactor-extract-tag-component
# Conflicts: # app/components/Filter/Panel.vue # app/components/Package/Card.vue # app/components/Package/TableRow.vue
2 parents 70472e0 + 8d99bb0 commit 1b38d5b

39 files changed

Lines changed: 1183 additions & 542 deletions

app/components/AppHeader.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const showMobileMenu = shallowRef(false)
1919
const route = useRoute()
2020
const isMobile = useIsMobile()
2121
const isSearchExpandedManually = shallowRef(false)
22-
const searchBoxRef = shallowRef<{ focus: () => void } | null>(null)
22+
const searchBoxRef = useTemplateRef('searchBoxRef')
2323
2424
// On search page, always show search expanded on mobile
2525
const isOnHomePage = computed(() => route.name === 'index')

app/components/CallToAction.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ const socialLinks = {
1212
{{ $t('about.get_involved.title') }}
1313
</h2>
1414

15-
<div class="grid gap-4 sm:grid-cols-3">
15+
<div class="grid gap-4 sm:grid-cols-3 sm:items-stretch sm:grid-rows-[auto,1fr,auto]">
1616
<a
1717
:href="socialLinks.github"
1818
target="_blank"
1919
rel="noopener noreferrer"
20-
class="group flex flex-col gap-3 p-4 rounded-lg bg-bg-subtle hover:bg-bg-elevated border border-border hover:border-border-hover transition-all duration-200"
20+
class="group grid gap-3 p-4 rounded-lg bg-bg-subtle hover:bg-bg-elevated border border-border hover:border-border-hover transition-all duration-200 sm:grid-rows-subgrid sm:row-span-3"
2121
>
2222
<div class="flex gap-2">
2323
<span class="i-carbon:logo-github shrink-0 mt-1 w-5 h-5 text-fg" aria-hidden="true" />
@@ -40,7 +40,7 @@ const socialLinks = {
4040
:href="socialLinks.discord"
4141
target="_blank"
4242
rel="noopener noreferrer"
43-
class="group flex flex-col gap-3 p-4 rounded-lg bg-bg-subtle hover:bg-bg-elevated border border-border hover:border-border-hover transition-all duration-200"
43+
class="group grid gap-3 p-4 rounded-lg bg-bg-subtle hover:bg-bg-elevated border border-border hover:border-border-hover transition-all duration-200 sm:grid-rows-subgrid sm:row-span-3"
4444
>
4545
<div class="flex gap-2">
4646
<span class="i-carbon:chat shrink-0 mt-1 w-5 h-5 text-fg" aria-hidden="true" />
@@ -63,7 +63,7 @@ const socialLinks = {
6363
:href="socialLinks.bluesky"
6464
target="_blank"
6565
rel="noopener noreferrer"
66-
class="group flex flex-col gap-3 p-4 rounded-lg bg-bg-subtle hover:bg-bg-elevated border border-border hover:border-border-hover transition-all duration-200"
66+
class="group grid gap-3 p-4 rounded-lg bg-bg-subtle hover:bg-bg-elevated border border-border hover:border-border-hover transition-all duration-200 sm:grid-rows-subgrid sm:row-span-3"
6767
>
6868
<div class="flex gap-2">
6969
<span class="i-simple-icons:bluesky shrink-0 mt-1 w-5 h-5 text-fg" aria-hidden="true" />

app/components/Filter/Panel.vue

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import type {
88
} from '#shared/types/preferences'
99
import {
1010
DOWNLOAD_RANGES,
11-
SEARCH_SCOPE_OPTIONS,
12-
SECURITY_FILTER_OPTIONS,
11+
SEARCH_SCOPE_VALUES,
12+
SECURITY_FILTER_VALUES,
1313
UPDATED_WITHIN_OPTIONS,
1414
} from '#shared/types/preferences'
1515
@@ -205,20 +205,20 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
205205
:aria-label="$t('filters.search_scope')"
206206
>
207207
<button
208-
v-for="option in SEARCH_SCOPE_OPTIONS"
209-
:key="option.value"
208+
v-for="scope in SEARCH_SCOPE_VALUES"
209+
:key="scope"
210210
type="button"
211211
class="px-2 py-0.5 text-xs font-mono rounded-sm transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
212212
:class="
213-
filters.searchScope === option.value
213+
filters.searchScope === scope
214214
? 'bg-bg-muted text-fg'
215215
: 'text-fg-muted hover:text-fg'
216216
"
217-
:aria-pressed="filters.searchScope === option.value"
218-
:title="$t(getScopeDescriptionKey(option.value))"
219-
@click="emit('update:searchScope', option.value)"
217+
:aria-pressed="filters.searchScope === scope"
218+
:title="$t(getScopeDescriptionKey(scope))"
219+
@click="emit('update:searchScope', scope)"
220220
>
221-
{{ $t(getScopeLabelKey(option.value)) }}
221+
{{ $t(getScopeLabelKey(scope)) }}
222222
</button>
223223
</div>
224224
</div>
@@ -291,15 +291,15 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
291291
</legend>
292292
<div class="flex flex-wrap gap-2" role="radiogroup" :aria-label="$t('filters.security')">
293293
<TagClickable
294-
v-for="option in SECURITY_FILTER_OPTIONS"
295-
:key="option.value"
294+
v-for="security in SECURITY_FILTER_VALUES"
295+
:key="security"
296296
type="button"
297297
role="radio"
298298
disabled
299-
:aria-checked="filters.security === option.value"
300-
:status="filters.security === option.value ? 'active' : 'default'"
299+
:aria-checked="filters.security === security"
300+
:status="filters.security === security ? 'active' : 'default'"
301301
>
302-
{{ $t(getSecurityLabelKey(option.value)) }}
302+
{{ $t(getSecurityLabelKey(security)) }}
303303
</TagClickable>
304304
</div>
305305
</fieldset>

app/components/Header/SearchBox.vue

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,29 @@ function handleSearchFocus() {
7979
emit('focus')
8080
}
8181
82+
function handleSubmit() {
83+
if (pagesWithLocalFilter.has(route.name as string)) {
84+
router.push({
85+
name: 'search',
86+
query: {
87+
q: searchQuery.value,
88+
},
89+
})
90+
} else {
91+
updateUrlQuery.flush()
92+
}
93+
}
94+
8295
// Expose focus method for parent components
83-
const inputRef = shallowRef<HTMLInputElement | null>(null)
96+
const inputRef = useTemplateRef('inputRef')
8497
function focus() {
8598
inputRef.value?.focus()
8699
}
87100
defineExpose({ focus })
88101
</script>
89102
<template>
90103
<search v-if="showSearchBar" :class="'flex-1 sm:max-w-md ' + inputClass">
91-
<form method="GET" action="/search" class="relative">
104+
<form method="GET" action="/search" class="relative" @submit.prevent="handleSubmit">
92105
<label for="header-search" class="sr-only">
93106
{{ $t('search.label') }}
94107
</label>

app/components/Modal.client.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const props = defineProps<{
33
modalTitle: string
44
}>()
55
6-
const dialogRef = ref<HTMLDialogElement>()
6+
const dialogRef = useTemplateRef('dialogRef')
77
88
const modalTitleId = computed(() => {
99
const id = getCurrentInstance()?.attrs.id

app/components/Package/Card.vue

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script setup lang="ts">
2+
import type { StructuredFilters } from '#shared/types/preferences'
3+
24
const props = defineProps<{
35
/** The search result object containing package data */
46
result: NpmSearchResult
@@ -8,10 +10,16 @@ const props = defineProps<{
810
showPublisher?: boolean
911
prefetch?: boolean
1012
index?: number
13+
/** Filters to apply to the results */
14+
filters?: StructuredFilters
1115
/** Search query for highlighting exact matches */
1216
searchQuery?: string
1317
}>()
1418
19+
const emit = defineEmits<{
20+
clickKeyword: [keyword: string]
21+
}>()
22+
1523
/** Check if this package is an exact match for the search query */
1624
const isExactMatch = computed(() => {
1725
if (!props.searchQuery) return false
@@ -149,14 +157,29 @@ const pkgDescription = useMarkdown(() => ({
149157
</div>
150158
</div>
151159

152-
<ul
160+
<div
153161
v-if="result.package.keywords?.length"
154162
:aria-label="$t('package.card.keywords')"
155-
class="relative z-10 flex flex-wrap gap-1.5 mt-3 pt-3 border-t border-border list-none m-0 p-0"
163+
class="relative z-10 flex flex-wrap gap-1.5 mt-3 pt-3 border-t border-border list-none m-0 p-0 pointer-events-none"
156164
>
157-
<TagStatic as="li" v-for="keyword in result.package.keywords.slice(0, 5)" :key="keyword">
165+
<TagClickable
166+
v-for="keyword in result.package.keywords.slice(0, 5)"
167+
:key="keyword"
168+
type="button"
169+
class="pointer-events-auto"
170+
:status="props.filters?.keywords.includes(keyword) ? 'active' : 'default'"
171+
:title="`Filter by ${keyword}`"
172+
@click.stop="emit('clickKeyword', keyword)"
173+
>
158174
{{ keyword }}
159-
</TagStatic>
160-
</ul>
175+
</TagClickable>
176+
<span
177+
v-if="result.package.keywords.length > 5"
178+
class="text-fg-subtle text-xs pointer-events-auto"
179+
:title="result.package.keywords.slice(5).join(', ')"
180+
>
181+
+{{ result.package.keywords.length - 5 }}
182+
</span>
183+
</div>
161184
</BaseCard>
162185
</template>

app/components/Package/ClaimPackageModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ async function handleClaim() {
9696
}
9797
}
9898
99-
const dialogRef = ref<HTMLDialogElement>()
99+
const dialogRef = useTemplateRef('dialogRef')
100100
101101
function open() {
102102
// Reset state and check availability each time modal is opened

app/components/Package/List.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const SSR_COUNT = 20
1717
const props = defineProps<{
1818
/** List of search results to display */
1919
results: NpmSearchResult[]
20+
/** Filters to apply to the results */
21+
filters?: StructuredFilters
2022
/** Heading level for package names */
2123
headingLevel?: 'h2' | 'h3'
2224
/** Whether to show publisher username on cards */
@@ -39,6 +41,8 @@ const props = defineProps<{
3941
paginationMode?: PaginationMode
4042
/** Current page (1-indexed) for paginated mode */
4143
currentPage?: number
44+
/** When true, shows search-specific UI (relevance sort, no filters) */
45+
searchContext?: boolean
4246
}>()
4347
4448
const emit = defineEmits<{
@@ -60,7 +64,11 @@ const sortOption = defineModel<SortOption>('sortOption')
6064
6165
// View mode and columns
6266
const viewMode = computed(() => props.viewMode ?? 'cards')
63-
const columns = computed(() => props.columns ?? DEFAULT_COLUMNS)
67+
const columns = computed(() => {
68+
const targetColumns = props.columns ?? DEFAULT_COLUMNS
69+
if (props.searchContext) return targetColumns.map(column => ({ ...column, sortable: false }))
70+
return targetColumns
71+
})
6472
// Table view forces pagination mode (no virtualization for tables)
6573
const paginationMode = computed(() =>
6674
viewMode.value === 'table' ? 'paginated' : (props.paginationMode ?? 'infinite'),
@@ -147,6 +155,7 @@ defineExpose({
147155
<template v-if="viewMode === 'table'">
148156
<PackageTable
149157
:results="displayedResults"
158+
:filters="filters"
150159
:columns="columns"
151160
v-model:sort-option="sortOption"
152161
:is-loading="isLoading"
@@ -176,7 +185,9 @@ defineExpose({
176185
:index="index"
177186
:search-query="searchQuery"
178187
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
188+
:filters="filters"
179189
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
190+
@click-keyword="emit('clickKeyword', $event)"
180191
/>
181192
</div>
182193
</template>
@@ -193,6 +204,8 @@ defineExpose({
193204
:show-publisher="showPublisher"
194205
:index="index"
195206
:search-query="searchQuery"
207+
:filters="filters"
208+
@click-keyword="emit('clickKeyword', $event)"
196209
/>
197210
</div>
198211
</li>
@@ -225,6 +238,8 @@ defineExpose({
225238
:search-query="searchQuery"
226239
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
227240
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
241+
:filters="filters"
242+
@click-keyword="emit('clickKeyword', $event)"
228243
/>
229244
</li>
230245
</ol>

app/components/Package/Table.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
<script setup lang="ts">
22
import type { NpmSearchResult } from '#shared/types/npm-registry'
3-
import type { ColumnConfig, ColumnId, SortKey, SortOption } from '#shared/types/preferences'
3+
import type {
4+
ColumnConfig,
5+
ColumnId,
6+
SortKey,
7+
SortOption,
8+
StructuredFilters,
9+
} from '#shared/types/preferences'
410
import { buildSortOption, parseSortOption, toggleDirection } from '#shared/types/preferences'
511
612
const props = defineProps<{
713
results: NpmSearchResult[]
814
columns: ColumnConfig[]
15+
filters?: StructuredFilters
916
isLoading?: boolean
1017
}>()
1118
@@ -317,6 +324,7 @@ function getColumnLabelKey(id: ColumnId): string {
317324
:result="result"
318325
:columns="columns"
319326
:index="index"
327+
:filters="filters"
320328
@click-keyword="emit('clickKeyword', $event)"
321329
/>
322330
</template>

app/components/Package/TableRow.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<script setup lang="ts">
22
import type { NpmSearchResult } from '#shared/types/npm-registry'
3-
import type { ColumnConfig } from '#shared/types/preferences'
3+
import type { ColumnConfig, StructuredFilters } from '#shared/types/preferences'
44
55
const props = defineProps<{
66
result: NpmSearchResult
77
columns: ColumnConfig[]
88
index?: number
9+
filters?: StructuredFilters
910
}>()
1011
1112
const emit = defineEmits<{
@@ -117,17 +118,26 @@ const allMaintainersText = computed(() => {
117118

118119
<!-- Keywords -->
119120
<td v-if="isColumnVisible('keywords')" class="py-2 px-3">
120-
<div v-if="pkg.keywords?.length" class="flex flex-wrap gap-1">
121+
<div
122+
v-if="pkg.keywords?.length"
123+
class="flex flex-wrap gap-1"
124+
:aria-label="$t('package.card.keywords')"
125+
>
121126
<TagClickable
122127
v-for="keyword in pkg.keywords.slice(0, 3)"
123128
:key="keyword"
124129
type="button"
130+
:status="props.filters?.keywords.includes(keyword) ? 'active' : 'default'"
125131
:title="`Filter by ${keyword}`"
126132
@click.stop="emit('clickKeyword', keyword)"
127133
>
128134
{{ keyword }}
129135
</TagClickable>
130-
<span v-if="pkg.keywords.length > 3" class="text-fg-subtle text-xs">
136+
<span
137+
v-if="pkg.keywords.length > 3"
138+
class="text-fg-subtle text-xs"
139+
:title="pkg.keywords.slice(3).join(', ')"
140+
>
131141
+{{ pkg.keywords.length - 3 }}
132142
</span>
133143
</div>

0 commit comments

Comments
 (0)