Skip to content

Commit 7ea4048

Browse files
committed
Merge branch 'main' into feat/add-ut
2 parents cb5484e + 0a2b395 commit 7ea4048

35 files changed

Lines changed: 1178 additions & 155 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ We welcome contributions – please do feel free to explore the project and
155155
- [npm-userscript](https://github.com/bluwy/npm-userscript) – Browser userscript with various improvements and fixes for npmjs.com
156156
- [npm-alt](https://npm.willow.sh/) – An alternative npm package browser
157157
- [npkg.lorypelli.dev](https://npkg.lorypelli.dev/) – An alternative frontend to npm made with as little client-side JavaScript as possible
158-
- [vscode-npmx](https://github.com/npmx-dev/vscode-npmx) – VSCode extension for npmx
158+
- [vscode-npmx](https://github.com/npmx-dev/vscode-npmx) – Official VSCode extension for npmx
159+
- [vscode-open-in-npmx](https://github.com/sybers/vscode-open-in-npmx) – VSCode shortcut to open packages on npmx
159160
- [nxjt](https://nxjt.netlify.app) – npmx Jump To: Quickly navigate to npmx common webpages.
160161
- [npmx-weekly](https://npmx-weekly.trueberryless.org/) – A weekly newsletter for the npmx ecosystem. Add your own content via suggestions in the weekly PR on [GitHub](https://github.com/trueberryless-org/npmx-weekly/pulls?q=is%3Aopen+is%3Apr+label%3A%22%F0%9F%95%94+weekly+post%22).
161162
- [npmx-digest](https://npmx-digest.trueberryless.org/) – An automated news aggregation website that summarizes npmx activity from GitHub and Bluesky every 8 hours.

app/app.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,12 @@ if (import.meta.client) {
121121
<template>
122122
<div class="min-h-screen flex flex-col bg-bg text-fg">
123123
<NuxtPwaAssets />
124-
<LinkBase to="#main-content" variant="button-primary" class="skip-link">{{
125-
$t('common.skip_link')
126-
}}</LinkBase>
124+
<LinkBase
125+
:to="{ hash: '#main-content', query: route.query, params: route.params }"
126+
variant="button-primary"
127+
class="skip-link"
128+
>{{ $t('common.skip_link') }}</LinkBase
129+
>
127130

128131
<AppHeader :show-logo="!isHomepage" />
129132

app/components/AppHeader.vue

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ const mobileLinks = computed<NavigationConfigWithGroups>(() => [
124124
125125
const showFullSearch = shallowRef(false)
126126
const showMobileMenu = shallowRef(false)
127+
const { env } = useAppConfig().buildInfo
127128
128129
// On mobile, clicking logo+search button expands search
129130
const route = useRoute()
@@ -194,31 +195,34 @@ onKeyStroke(
194195
<div class="absolute inset-0 bg-bg/80 backdrop-blur-md" />
195196
<nav
196197
:aria-label="$t('nav.main_navigation')"
197-
class="relative container min-h-14 flex items-center gap-2 z-1"
198-
:class="isOnHomePage ? 'justify-end' : 'justify-between'"
198+
class="relative container min-h-14 flex items-center gap-2 z-1 justify-end"
199199
>
200-
<!-- Mobile: Logo + search button (expands search, doesn't navigate) -->
201-
<button
200+
<!-- Mobile: Logo (navigates home) -->
201+
<NuxtLink
202202
v-if="!isSearchExpanded && !isOnHomePage"
203-
type="button"
204-
class="sm:hidden flex-shrink-0 inline-flex items-center gap-2 font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 rounded"
205-
:aria-label="$t('nav.tap_to_search')"
206-
@click="expandMobileSearch"
203+
to="/"
204+
:aria-label="$t('header.home')"
205+
class="sm:hidden flex-shrink-0 font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring"
207206
>
208207
<AppLogo class="w-8 h-8 rounded-lg" />
209-
<span class="i-carbon:search w-4 h-4 text-fg-subtle" aria-hidden="true" />
210-
</button>
208+
</NuxtLink>
211209

212210
<!-- Desktop: Logo (navigates home) -->
213211
<div v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
214212
<NuxtLink
215213
:to="{ name: 'index' }"
216214
:aria-label="$t('header.home')"
217215
dir="ltr"
218-
class="inline-flex items-center gap-1 header-logo font-mono text-lg font-medium text-fg hover:text-fg/90 transition-colors duration-200 rounded"
216+
class="relative inline-flex items-center gap-1 header-logo font-mono text-lg font-medium text-fg hover:text-fg/90 transition-colors duration-200 rounded"
219217
>
220-
<AppLogo class="w-8 h-8 rounded-lg" />
221-
<span>npmx</span>
218+
<AppLogo class="w-7 h-7 rounded-lg" />
219+
<span class="pb-0.5">npmx</span>
220+
<span
221+
aria-hidden="true"
222+
class="scale-35 transform-origin-br font-mono tracking-wide text-accent absolute bottom-0.5 -inset-ie-1"
223+
>
224+
{{ env === 'release' ? 'alpha' : env }}
225+
</span>
222226
</NuxtLink>
223227
</div>
224228
<!-- Spacer when logo is hidden on desktop -->
@@ -275,6 +279,17 @@ onKeyStroke(
275279
<HeaderAccountMenu />
276280
</div>
277281

282+
<!-- Mobile: Search button (expands search) -->
283+
<ButtonBase
284+
type="button"
285+
class="sm:hidden ms-auto"
286+
:aria-label="$t('nav.tap_to_search')"
287+
:aria-expanded="showMobileMenu"
288+
@click="expandMobileSearch"
289+
v-if="!isSearchExpanded && !isOnHomePage"
290+
classicon="i-carbon:search"
291+
/>
292+
278293
<!-- Mobile: Menu button (always visible, click to open menu) -->
279294
<ButtonBase
280295
type="button"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<script setup lang="ts">
2+
import { BLUESKY_API, BSKY_POST_AT_URI_REGEX } from '#shared/utils/constants'
3+
4+
const props = defineProps<{
5+
/** AT URI of the post, e.g. at://did:plc:.../app.bsky.feed.post/... */
6+
uri: string
7+
}>()
8+
9+
interface PostAuthor {
10+
did: string
11+
handle: string
12+
displayName?: string
13+
avatar?: string
14+
}
15+
16+
interface EmbedImage {
17+
thumb: string
18+
fullsize: string
19+
alt: string
20+
aspectRatio?: { width: number; height: number }
21+
}
22+
23+
interface BlueskyPost {
24+
uri: string
25+
author: PostAuthor
26+
record: { text: string; createdAt: string }
27+
embed?: { $type: string; images?: EmbedImage[] }
28+
likeCount?: number
29+
replyCount?: number
30+
repostCount?: number
31+
}
32+
33+
const postUrl = computed(() => {
34+
const match = props.uri.match(BSKY_POST_AT_URI_REGEX)
35+
if (!match) return null
36+
const [, did, rkey] = match
37+
return `https://bsky.app/profile/${did}/post/${rkey}`
38+
})
39+
40+
const { data: post, status } = useAsyncData(
41+
`bsky-post-${props.uri}`,
42+
async (): Promise<BlueskyPost | null> => {
43+
const response = await $fetch<{ posts: BlueskyPost[] }>(
44+
`${BLUESKY_API}/xrpc/app.bsky.feed.getPosts`,
45+
{ query: { uris: props.uri } },
46+
)
47+
return response.posts[0] ?? null
48+
},
49+
{ lazy: true, server: false },
50+
)
51+
</script>
52+
53+
<template>
54+
<div
55+
v-if="status === 'pending'"
56+
class="rounded-lg border border-border bg-bg-subtle p-6 text-center text-fg-subtle text-sm"
57+
>
58+
<span class="i-svg-spinners:90-ring-with-bg h-5 w-5 inline-block" />
59+
</div>
60+
61+
<a
62+
v-else-if="post"
63+
:href="postUrl ?? '#'"
64+
target="_blank"
65+
rel="noopener noreferrer"
66+
class="block rounded-lg border border-border bg-bg-subtle p-4 sm:p-5 no-underline hover:border-border-hover transition-colors duration-200"
67+
>
68+
<!-- Author row -->
69+
<div class="flex items-center gap-3 mb-3">
70+
<img
71+
v-if="post.author.avatar"
72+
:src="`${post.author.avatar}?size=48`"
73+
:alt="post.author.displayName || post.author.handle"
74+
width="40"
75+
height="40"
76+
class="w-10 h-10 rounded-full"
77+
loading="lazy"
78+
/>
79+
<div class="min-w-0">
80+
<div class="font-medium text-fg truncate">
81+
{{ post.author.displayName || post.author.handle }}
82+
</div>
83+
<div class="text-sm text-fg-subtle truncate">@{{ post.author.handle }}</div>
84+
</div>
85+
<span
86+
class="i-carbon:logo-bluesky w-5 h-5 text-fg-subtle ms-auto shrink-0"
87+
aria-hidden="true"
88+
/>
89+
</div>
90+
91+
<!-- Post text -->
92+
<p class="text-fg-muted whitespace-pre-wrap leading-relaxed mb-3">{{ post.record.text }}</p>
93+
94+
<!-- Embedded images -->
95+
<template v-if="post.embed?.images?.length">
96+
<img
97+
v-for="(img, i) in post.embed.images"
98+
:key="i"
99+
:src="img.fullsize"
100+
:alt="img.alt"
101+
class="w-full mb-3 rounded-lg object-cover"
102+
:style="
103+
img.aspectRatio
104+
? { aspectRatio: `${img.aspectRatio.width}/${img.aspectRatio.height}` }
105+
: undefined
106+
"
107+
loading="lazy"
108+
/>
109+
</template>
110+
111+
<!-- Timestamp + engagement -->
112+
<div class="flex items-center gap-4 text-sm text-fg-subtle">
113+
<DateTime :datetime="post.record.createdAt" date-style="medium" />
114+
<span v-if="post.likeCount" class="flex items-center gap-1">
115+
<span class="i-carbon:favorite w-3.5 h-3.5" aria-hidden="true" />
116+
{{ post.likeCount }}
117+
</span>
118+
<span v-if="post.repostCount" class="flex items-center gap-1">
119+
<span class="i-carbon:repeat w-3.5 h-3.5" aria-hidden="true" />
120+
{{ post.repostCount }}
121+
</span>
122+
<span v-if="post.replyCount" class="flex items-center gap-1">
123+
<span class="i-carbon:chat w-3.5 h-3.5" aria-hidden="true" />
124+
{{ post.replyCount }}
125+
</span>
126+
</div>
127+
</a>
128+
</template>

app/components/Header/SearchBox.vue

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
<script setup lang="ts">
2-
import { debounce } from 'perfect-debounce'
3-
import { normalizeSearchParam } from '#shared/utils/url'
4-
52
withDefaults(
63
defineProps<{
74
inputClass?: string
@@ -12,80 +9,17 @@ withDefaults(
129
)
1310
1411
const emit = defineEmits(['blur', 'focus'])
15-
16-
const router = useRouter()
1712
const route = useRoute()
18-
const { searchProvider } = useSearchProvider()
19-
const searchProviderValue = computed(() => {
20-
const p = normalizeSearchParam(route.query.p)
21-
if (p === 'npm' || searchProvider.value === 'npm') return 'npm'
22-
return 'algolia'
23-
})
24-
2513
const isSearchFocused = shallowRef(false)
2614
2715
const showSearchBar = computed(() => {
2816
return route.name !== 'index'
2917
})
3018
31-
const searchQuery = useGlobalSearchQuery()
32-
33-
// Pages that have their own local filter using ?q
34-
const pagesWithLocalFilter = new Set(['~username', 'org'])
35-
36-
function updateUrlQueryImpl(value: string, provider: 'npm' | 'algolia') {
37-
// Don't navigate away from pages that use ?q for local filtering
38-
if (pagesWithLocalFilter.has(route.name as string)) {
39-
return
40-
}
41-
if (route.name === 'search') {
42-
router.replace({ query: { q: value || undefined, p: provider === 'npm' ? 'npm' : undefined } })
43-
return
44-
}
45-
if (!value) {
46-
return
47-
}
48-
49-
router.push({
50-
name: 'search',
51-
query: {
52-
q: value,
53-
p: provider === 'npm' ? 'npm' : undefined,
54-
},
55-
})
56-
}
57-
58-
const updateUrlQueryNpm = debounce(updateUrlQueryImpl, 250)
59-
const updateUrlQueryAlgolia = debounce(updateUrlQueryImpl, 80)
60-
61-
const updateUrlQuery = Object.assign(
62-
(value: string) =>
63-
(searchProviderValue.value === 'algolia' ? updateUrlQueryAlgolia : updateUrlQueryNpm)(
64-
value,
65-
searchProviderValue.value,
66-
),
67-
{
68-
flush: () =>
69-
(searchProviderValue.value === 'algolia' ? updateUrlQueryAlgolia : updateUrlQueryNpm).flush(),
70-
},
71-
)
72-
73-
watch(searchQuery, value => {
74-
updateUrlQuery(value)
75-
})
19+
const { model: searchQuery, flushUpdateUrlQuery } = useGlobalSearch()
7620
7721
function handleSubmit() {
78-
if (pagesWithLocalFilter.has(route.name as string)) {
79-
router.push({
80-
name: 'search',
81-
query: {
82-
q: searchQuery.value,
83-
p: searchProviderValue.value === 'npm' ? 'npm' : undefined,
84-
},
85-
})
86-
} else {
87-
updateUrlQuery.flush()
88-
}
22+
flushUpdateUrlQuery()
8923
}
9024
9125
// Expose focus method for parent components

app/components/Package/Keywords.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
defineProps<{
33
keywords?: string[]
44
}>()
5+
6+
const { model } = useGlobalSearch()
57
</script>
68
<template>
79
<CollapsibleSection v-if="keywords?.length" :title="$t('package.keywords_title')" id="keywords">
@@ -10,7 +12,8 @@ defineProps<{
1012
<LinkBase
1113
variant="button-secondary"
1214
size="small"
13-
:to="{ name: 'search', query: { q: `keywords:${keyword}` } }"
15+
:to="{ name: 'search', query: { q: `keyword:${keyword}` } }"
16+
@click="model = `keyword:${keyword}`"
1417
>
1518
{{ keyword }}
1619
</LinkBase>

0 commit comments

Comments
 (0)