Skip to content

Commit d667bae

Browse files
Merge branch 'main' into feat/invalid-query-length-message
2 parents bf908d3 + 0b157cf commit d667bae

24 files changed

Lines changed: 329 additions & 125 deletions

.github/workflows/provenance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ permissions:
1414
contents: read
1515
jobs:
1616
check-provenance:
17-
runs-on: ubuntu-latest
17+
runs-on: ubuntu-slim
1818
steps:
1919
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
2020
with:

.github/workflows/semantic-pull-requests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs
1717
statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
1818
if: github.repository == 'npmx-dev/npmx.dev'
19-
runs-on: ubuntu-latest
19+
runs-on: ubuntu-slim
2020
name: semantic-pr
2121
steps:
2222
- name: Validate PR title

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/Header/SearchBox.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ 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
8396
const inputRef = useTemplateRef('inputRef')
8497
function focus() {
@@ -88,7 +101,7 @@ 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/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-
<li v-for="keyword in result.package.keywords.slice(0, 5)" :key="keyword" class="tag">
165+
<button
166+
v-for="keyword in result.package.keywords.slice(0, 5)"
167+
:key="keyword"
168+
type="button"
169+
class="tag text-xs hover:bg-fg hover:text-bg hover:border-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1 border-solid pointer-events-auto"
170+
:class="{ 'bg-fg text-bg hover:opacity-80': props.filters?.keywords.includes(keyword) }"
171+
:title="`Filter by ${keyword}`"
172+
@click.stop="emit('clickKeyword', keyword)"
173+
>
158174
{{ keyword }}
159-
</li>
160-
</ul>
175+
</button>
176+
<span
177+
v-if="result.package.keywords.length > 5"
178+
class="tag text-fg-subtle text-xs border-none bg-transparent 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/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: 14 additions & 4 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,18 +118,27 @@ 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
<button
122127
v-for="keyword in pkg.keywords.slice(0, 3)"
123128
:key="keyword"
124129
type="button"
125-
class="tag text-xs hover:bg-fg hover:text-bg hover:border-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
130+
class="tag text-xs hover:bg-fg hover:text-bg hover:border-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1 border-solid"
131+
:class="{ 'bg-fg text-bg hover:opacity-80': props.filters?.keywords.includes(keyword) }"
126132
:title="`Filter by ${keyword}`"
127133
@click.stop="emit('clickKeyword', keyword)"
128134
>
129135
{{ keyword }}
130136
</button>
131-
<span v-if="pkg.keywords.length > 3" class="text-fg-subtle text-xs">
137+
<span
138+
v-if="pkg.keywords.length > 3"
139+
class="tag text-fg-subtle text-xs border-none bg-transparent"
140+
:title="pkg.keywords.slice(3).join(', ')"
141+
>
132142
+{{ pkg.keywords.length - 3 }}
133143
</span>
134144
</div>

app/composables/useJsonLd.ts

Lines changed: 4 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,9 @@
1-
// JSON-LD Schema Types
2-
interface JsonLdBase {
3-
'@context': 'https://schema.org'
4-
'@type': string
5-
}
6-
7-
interface WebSiteSchema extends JsonLdBase {
8-
'@type': 'WebSite'
9-
'name': string
10-
'url': string
11-
'description'?: string
12-
'potentialAction'?: SearchActionSchema
13-
}
14-
15-
interface SearchActionSchema {
16-
'@type': 'SearchAction'
17-
'target': {
18-
'@type': 'EntryPoint'
19-
'urlTemplate': string
20-
}
21-
'query-input': string
22-
}
23-
24-
interface SoftwareApplicationSchema extends JsonLdBase {
25-
'@type': 'SoftwareApplication'
26-
'name': string
27-
'description'?: string
28-
'applicationCategory': 'DeveloperApplication'
29-
'operatingSystem': 'Cross-platform'
30-
'url': string
31-
'softwareVersion'?: string
32-
'dateModified'?: string
33-
'datePublished'?: string
34-
'license'?: string
35-
'author'?: PersonSchema | OrganizationSchema | (PersonSchema | OrganizationSchema)[]
36-
'maintainer'?: PersonSchema | OrganizationSchema | (PersonSchema | OrganizationSchema)[]
37-
'offers'?: OfferSchema
38-
'downloadUrl'?: string
39-
'codeRepository'?: string
40-
'keywords'?: string[]
41-
}
42-
43-
interface PersonSchema extends JsonLdBase {
44-
'@type': 'Person'
45-
'name': string
46-
'url'?: string
47-
}
48-
49-
interface OrganizationSchema extends JsonLdBase {
50-
'@type': 'Organization'
51-
'name': string
52-
'url'?: string
53-
'logo'?: string
54-
'description'?: string
55-
'sameAs'?: string[]
56-
}
57-
58-
interface OfferSchema {
59-
'@type': 'Offer'
60-
'price': string
61-
'priceCurrency': string
62-
}
63-
64-
interface BreadcrumbListSchema extends JsonLdBase {
65-
'@type': 'BreadcrumbList'
66-
'itemListElement': BreadcrumbItemSchema[]
67-
}
68-
69-
interface BreadcrumbItemSchema {
70-
'@type': 'ListItem'
71-
'position': number
72-
'name': string
73-
'item'?: string
74-
}
75-
76-
interface ProfilePageSchema extends JsonLdBase {
77-
'@type': 'ProfilePage'
78-
'name': string
79-
'url': string
80-
'mainEntity': PersonSchema | OrganizationSchema
81-
}
82-
83-
type JsonLdSchema =
84-
| WebSiteSchema
85-
| SoftwareApplicationSchema
86-
| PersonSchema
87-
| OrganizationSchema
88-
| BreadcrumbListSchema
89-
| ProfilePageSchema
1+
import type { Thing, WebSite, WithContext } from 'schema-dts'
902

913
/**
924
* Inject JSON-LD script into head
935
*/
94-
export function setJsonLd(schema: JsonLdSchema | JsonLdSchema[]) {
6+
export function setJsonLd(schema: WithContext<Thing> | WithContext<Thing>[]): void {
957
const schemas = Array.isArray(schema) ? schema : [schema]
968

979
useHead({
@@ -109,7 +21,7 @@ export function setJsonLd(schema: JsonLdSchema | JsonLdSchema[]) {
10921
export function createWebSiteSchema(options?: {
11022
name?: string
11123
description?: string
112-
}): WebSiteSchema {
24+
}): WithContext<WebSite> {
11325
const siteUrl = 'https://npmx.dev'
11426
return {
11527
'@context': 'https://schema.org',
@@ -123,7 +35,7 @@ export function createWebSiteSchema(options?: {
12335
'@type': 'EntryPoint',
12436
'urlTemplate': `${siteUrl}/search?q={search_term_string}`,
12537
},
126-
'query-input': 'required name=search_term_string',
38+
'query': 'required name=search_term_string',
12739
},
12840
}
12941
}

0 commit comments

Comments
 (0)