Skip to content

Commit 5e32d04

Browse files
committed
Merge branch 'feat/changelog-1' of github.com:WilcoSp/npmx.dev into feat/changelog-1
2 parents bc0fa2f + f4c84ec commit 5e32d04

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2492
-416
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ file-tree-sprite.svg
4545

4646
# output
4747
.vercel
48+
.nvmrc

.node-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization.
416416
- All user-facing strings should use translation keys via `$t()` in templates and script
417417
- Translation files live in [`i18n/locales/`](i18n/locales) (e.g., `en-US.json`)
418418
- We use the `no_prefix` strategy (no `/en-US/` or `/fr-FR/` in URLs)
419-
- Locale preference is stored in cookies and respected on subsequent visits
419+
- Locale preference is stored in `localStorage` and respected on subsequent visits
420420

421421
### i18n commands
422422

app/components/AppHeader.vue

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,20 +194,17 @@ onKeyStroke(
194194
<div class="absolute inset-0 bg-bg/80 backdrop-blur-md" />
195195
<nav
196196
: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'"
197+
class="relative container min-h-14 flex items-center gap-2 z-1 justify-end"
199198
>
200-
<!-- Mobile: Logo + search button (expands search, doesn't navigate) -->
201-
<button
199+
<!-- Mobile: Logo (navigates home) -->
200+
<NuxtLink
202201
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"
202+
to="/"
203+
:aria-label="$t('header.home')"
204+
class="sm:hidden flex-shrink-0 font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring"
207205
>
208206
<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>
207+
</NuxtLink>
211208

212209
<!-- Desktop: Logo (navigates home) -->
213210
<div v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
@@ -275,6 +272,17 @@ onKeyStroke(
275272
<HeaderAccountMenu />
276273
</div>
277274

275+
<!-- Mobile: Search button (expands search) -->
276+
<ButtonBase
277+
type="button"
278+
class="sm:hidden ms-auto"
279+
:aria-label="$t('nav.tap_to_search')"
280+
:aria-expanded="showMobileMenu"
281+
@click="expandMobileSearch"
282+
v-if="!isSearchExpanded && !isOnHomePage"
283+
classicon="i-carbon:search"
284+
/>
285+
278286
<!-- Mobile: Menu button (always visible, click to open menu) -->
279287
<ButtonBase
280288
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/Compare/FacetSelector.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,12 @@ function isCategoryNoneSelected(category: string): boolean {
6969
:disabled="facet.comingSoon"
7070
:aria-pressed="isFacetSelected(facet.id)"
7171
:aria-label="facet.label"
72-
class="inline-flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs rounded border transition-colors duration-200 focus-visible:outline-accent/70"
72+
class="gap-1 px-1.5 rounded transition-colors focus-visible:outline-accent/70"
7373
:class="
7474
facet.comingSoon
7575
? 'text-fg-subtle/50 bg-bg-subtle border-border-subtle cursor-not-allowed'
7676
: isFacetSelected(facet.id)
77-
? 'text-fg-muted bg-bg-muted border-border'
77+
? 'text-fg-muted bg-bg-muted'
7878
: 'text-fg-subtle bg-bg-subtle border-border-subtle hover:text-fg-muted hover:border-border'
7979
"
8080
@click="!facet.comingSoon && toggleFacet(facet.id)"

app/components/Header/ConnectorModal.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
const { isConnected, isConnecting, npmUser, error, hasOperations, connect, disconnect } =
33
useConnector()
44
5+
const { settings } = useSettings()
6+
57
const tokenInput = shallowRef('')
68
const portInput = shallowRef('31415')
79
const { copied, copy } = useClipboard({ copiedDuring: 2000 })
@@ -61,6 +63,16 @@ function handleDisconnect() {
6163
</div>
6264
</div>
6365

66+
<!-- Connector preferences -->
67+
<div class="flex flex-col gap-2">
68+
<SettingsToggle
69+
:label="$t('connector.modal.auto_open_url')"
70+
v-model="settings.connector.autoOpenURL"
71+
/>
72+
</div>
73+
74+
<div class="border-t border-border my-3" />
75+
6476
<!-- Operations Queue -->
6577
<OrgOperationsQueue />
6678

@@ -194,6 +206,14 @@ function handleDisconnect() {
194206
class="w-full"
195207
size="medium"
196208
/>
209+
210+
<div class="border-t border-border my-3" />
211+
<div class="flex flex-col gap-2">
212+
<SettingsToggle
213+
:label="$t('connector.modal.auto_open_url')"
214+
v-model="settings.connector.autoOpenURL"
215+
/>
216+
</div>
197217
</div>
198218
</details>
199219
</div>

app/components/Header/SearchBox.vue

Lines changed: 2 additions & 84 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,96 +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-
// Local input value (updates immediately as user types)
32-
const searchQuery = shallowRef(normalizeSearchParam(route.query.q))
33-
34-
// Pages that have their own local filter using ?q
35-
const pagesWithLocalFilter = new Set(['~username', 'org'])
36-
37-
function updateUrlQueryImpl(value: string, provider: 'npm' | 'algolia') {
38-
// Don't navigate away from pages that use ?q for local filtering
39-
if (pagesWithLocalFilter.has(route.name as string)) {
40-
return
41-
}
42-
if (route.name === 'search') {
43-
router.replace({ query: { q: value || undefined, p: provider === 'npm' ? 'npm' : undefined } })
44-
return
45-
}
46-
if (!value) {
47-
return
48-
}
49-
50-
router.push({
51-
name: 'search',
52-
query: {
53-
q: value,
54-
p: provider === 'npm' ? 'npm' : undefined,
55-
},
56-
})
57-
}
58-
59-
const updateUrlQueryNpm = debounce(updateUrlQueryImpl, 250)
60-
const updateUrlQueryAlgolia = debounce(updateUrlQueryImpl, 80)
61-
62-
const updateUrlQuery = Object.assign(
63-
(value: string) =>
64-
(searchProviderValue.value === 'algolia' ? updateUrlQueryAlgolia : updateUrlQueryNpm)(
65-
value,
66-
searchProviderValue.value,
67-
),
68-
{
69-
flush: () =>
70-
(searchProviderValue.value === 'algolia' ? updateUrlQueryAlgolia : updateUrlQueryNpm).flush(),
71-
},
72-
)
73-
74-
watch(searchQuery, value => {
75-
updateUrlQuery(value)
76-
})
77-
78-
// Sync input with URL when navigating (e.g., back button)
79-
watch(
80-
() => route.query.q,
81-
urlQuery => {
82-
// Don't sync from pages that use ?q for local filtering
83-
if (pagesWithLocalFilter.has(route.name as string)) {
84-
return
85-
}
86-
const value = normalizeSearchParam(urlQuery)
87-
if (searchQuery.value !== value) {
88-
searchQuery.value = value
89-
}
90-
},
91-
)
19+
const { model: searchQuery, flushUpdateUrlQuery } = useGlobalSearch()
9220
9321
function handleSubmit() {
94-
if (pagesWithLocalFilter.has(route.name as string)) {
95-
router.push({
96-
name: 'search',
97-
query: {
98-
q: searchQuery.value,
99-
p: searchProviderValue.value === 'npm' ? 'npm' : undefined,
100-
},
101-
})
102-
} else {
103-
updateUrlQuery.flush()
104-
}
22+
flushUpdateUrlQuery()
10523
}
10624
10725
// Expose focus method for parent components

0 commit comments

Comments
 (0)