Skip to content

Commit 73169c0

Browse files
authored
Merge branch 'main' into feature/provenance
2 parents 0490129 + f5ae776 commit 73169c0

157 files changed

Lines changed: 8313 additions & 1016 deletions

File tree

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ on:
1111
branches:
1212
- main
1313

14+
# cancel in-progress runs on new commits to same PR (gitub.event.number)
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
17+
cancel-in-progress: true
18+
1419
permissions:
1520
contents: read
1621

@@ -134,4 +139,4 @@ jobs:
134139
run: pnpm install
135140

136141
- name: 🔍 Check for unused code
137-
run: pnpm knip:production
142+
run: pnpm knip

CONTRIBUTING.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ The connector will check your npm authentication, generate a connection token, a
105105

106106
## Code style
107107

108+
When committing changes, try to keep an eye out for unintended formatting updates. These can make a pull request look noisier than it really is and slow down the review process. Sometimes IDEs automatically reformat files on save, which can unintentionally introduce extra changes.
109+
110+
To help with this, the project uses `oxfmt` to handle formatting via a pre-commit hook. The hook will automatically reformat files when needed. If something can’t be fixed automatically, it will let you know what needs to be updated before you can commit.
111+
112+
If you want to get ahead of any formatting issues, you can also run `pnpm lint:fix` before committing to fix formatting across the whole project.
113+
108114
### Typescript
109115

110116
- We care about good types – never cast things to `any` 💪
@@ -369,13 +375,17 @@ We recommend the [i18n-ally](https://marketplace.visualstudio.com/items?itemName
369375

370376
The extension is included in our workspace recommendations, so VSCode should prompt you to install it.
371377

372-
### Formatting with locale
378+
### Formatting numbers and dates
373379

374-
When formatting numbers or dates that should respect the user's locale, pass the locale:
380+
Use vue-i18n's built-in formatters for locale-aware formatting:
375381

376-
```typescript
377-
const { locale } = useI18n()
378-
const formatted = formatNumber(12345, locale.value) // "12,345" in en-US
382+
```vue
383+
<template>
384+
<p>{{ $n(12345) }}</p>
385+
<!-- "12,345" in en-US, "12 345" in fr-FR -->
386+
<p>{{ $d(new Date()) }}</p>
387+
<!-- locale-aware date -->
388+
</template>
379389
```
380390

381391
## Testing

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2026 Daniel Roe
3+
Copyright (c) 2026 npmx team and contributors
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

app/components/AppFooter.vue

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,16 @@ const isHome = computed(() => route.name === 'index')
1313
<p class="font-mono text-balance m-0 hidden sm:block">{{ $t('tagline') }}</p>
1414
<BuildEnvironment v-if="!isHome" footer />
1515
</div>
16-
<div class="flex items-center gap-3 sm:gap-6">
17-
<NuxtLink
18-
to="/about"
19-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"
20-
>
16+
<!-- Desktop: Show all links. Mobile: Links are in MobileMenu -->
17+
<div class="hidden sm:flex items-center gap-6">
18+
<NuxtLink to="/about" class="link-subtle font-mono text-xs min-h-11 flex items-center">
2119
{{ $t('footer.about') }}
2220
</NuxtLink>
2321
<a
2422
href="https://docs.npmx.dev"
2523
target="_blank"
2624
rel="noopener noreferrer"
27-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
25+
class="link-subtle font-mono text-xs min-h-11 flex items-center gap-1"
2826
>
2927
{{ $t('footer.docs') }}
3028
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
@@ -33,7 +31,7 @@ const isHome = computed(() => route.name === 'index')
3331
href="https://repo.npmx.dev"
3432
target="_blank"
3533
rel="noopener noreferrer"
36-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
34+
class="link-subtle font-mono text-xs min-h-11 flex items-center gap-1"
3735
>
3836
{{ $t('footer.source') }}
3937
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
@@ -42,7 +40,7 @@ const isHome = computed(() => route.name === 'index')
4240
href="https://social.npmx.dev"
4341
target="_blank"
4442
rel="noopener noreferrer"
45-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
43+
class="link-subtle font-mono text-xs min-h-11 flex items-center gap-1"
4644
>
4745
{{ $t('footer.social') }}
4846
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
@@ -51,7 +49,7 @@ const isHome = computed(() => route.name === 'index')
5149
href="https://chat.npmx.dev"
5250
target="_blank"
5351
rel="noopener noreferrer"
54-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
52+
class="link-subtle font-mono text-xs min-h-11 flex items-center gap-1"
5553
>
5654
{{ $t('footer.chat') }}
5755
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />

app/components/AppHeader.vue

Lines changed: 157 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,53 @@ withDefaults(
1111
const { isConnected, npmUser } = useConnector()
1212
1313
const showFullSearch = shallowRef(false)
14+
const showMobileMenu = shallowRef(false)
15+
16+
// On mobile, clicking logo+search button expands search
17+
const route = useRoute()
18+
const isMobile = useIsMobile()
19+
const isSearchExpandedManually = shallowRef(false)
20+
const searchBoxRef = shallowRef<{ focus: () => void } | null>(null)
21+
22+
// On search page, always show search expanded on mobile
23+
const isOnHomePage = computed(() => route.name === 'index')
24+
const isOnSearchPage = computed(() => route.name === 'search')
25+
const isSearchExpanded = computed(() => isOnSearchPage.value || isSearchExpandedManually.value)
26+
27+
function expandMobileSearch() {
28+
isSearchExpandedManually.value = true
29+
nextTick(() => {
30+
searchBoxRef.value?.focus()
31+
})
32+
}
33+
34+
watch(
35+
isOnSearchPage,
36+
visible => {
37+
if (!visible) return
38+
39+
searchBoxRef.value?.focus()
40+
nextTick(() => {
41+
searchBoxRef.value?.focus()
42+
})
43+
},
44+
{ flush: 'sync' },
45+
)
46+
47+
function handleSearchBlur() {
48+
showFullSearch.value = false
49+
// Collapse expanded search on mobile after blur (with delay for click handling)
50+
// But don't collapse if we're on the search page
51+
if (isMobile.value && !isOnSearchPage.value) {
52+
setTimeout(() => {
53+
isSearchExpandedManually.value = false
54+
}, 150)
55+
}
56+
}
57+
58+
function handleSearchFocus() {
59+
showFullSearch.value = true
60+
}
1461
1562
onKeyStroke(
1663
',',
@@ -26,49 +73,91 @@ onKeyStroke(
2673
},
2774
{ dedupe: true },
2875
)
76+
77+
onKeyStroke(
78+
'c',
79+
e => {
80+
// Allow more specific handlers to take precedence
81+
if (e.defaultPrevented) return
82+
83+
// Don't trigger if user is typing in an input
84+
const target = e.target as HTMLElement
85+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
86+
return
87+
}
88+
89+
e.preventDefault()
90+
navigateTo('/compare')
91+
},
92+
{ dedupe: true },
93+
)
2994
</script>
3095

3196
<template>
3297
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
3398
<nav
3499
:aria-label="$t('nav.main_navigation')"
35-
class="container h-14 flex items-center justify-start"
100+
class="container min-h-14 flex items-center gap-2"
101+
:class="isOnHomePage ? 'justify-end' : 'justify-between'"
36102
>
37-
<!-- Start: Logo -->
38-
<div :class="{ 'hidden sm:block': showFullSearch }" class="flex-shrink-0">
39-
<div v-if="showLogo">
40-
<NuxtLink
41-
to="/"
42-
:aria-label="$t('header.home')"
43-
dir="ltr"
44-
class="inline-flex items-center gap-2 header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
45-
>
46-
<img
47-
aria-hidden="true"
48-
:alt="$t('alt_logo')"
49-
src="/logo.svg"
50-
width="96"
51-
height="96"
52-
class="w-8 h-8 rounded-lg"
53-
/>
54-
<span>npmx</span>
55-
</NuxtLink>
56-
</div>
57-
<!-- Spacer when logo is hidden -->
58-
<span v-else class="w-1" />
103+
<!-- Mobile: Logo + search button (expands search, doesn't navigate) -->
104+
<button
105+
v-if="!isSearchExpanded && !isOnHomePage"
106+
type="button"
107+
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 focus-ring rounded"
108+
:aria-label="$t('nav.tap_to_search')"
109+
@click="expandMobileSearch"
110+
>
111+
<img
112+
aria-hidden="true"
113+
:alt="$t('alt_logo')"
114+
src="/logo.svg"
115+
width="96"
116+
height="96"
117+
class="w-8 h-8 rounded-lg"
118+
/>
119+
<span class="i-carbon:search w-4 h-4 text-fg-subtle" aria-hidden="true" />
120+
</button>
121+
122+
<!-- Desktop: Logo (navigates home) -->
123+
<div v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
124+
<NuxtLink
125+
to="/"
126+
:aria-label="$t('header.home')"
127+
dir="ltr"
128+
class="inline-flex items-center gap-2 header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
129+
>
130+
<img
131+
aria-hidden="true"
132+
:alt="$t('alt_logo')"
133+
src="/logo.svg"
134+
width="96"
135+
height="96"
136+
class="w-8 h-8 rounded-lg"
137+
/>
138+
<span>npmx</span>
139+
</NuxtLink>
59140
</div>
141+
<!-- Spacer when logo is hidden on desktop -->
142+
<span v-else class="hidden sm:block w-1" />
60143

61144
<!-- Center: Search bar + nav items -->
62-
<div class="flex-1 flex items-center justify-center md:gap-6 mx-2">
63-
<!-- Search bar (shown on all pages except home) -->
145+
<div
146+
class="flex-1 flex items-center justify-center md:gap-6"
147+
:class="{ 'hidden sm:flex': !isSearchExpanded }"
148+
>
149+
<!-- Search bar (hidden on mobile unless expanded) -->
64150
<SearchBox
65-
:inputClass="showFullSearch ? '' : 'max-w[6rem]'"
66-
@focus="showFullSearch = true"
67-
@blur="showFullSearch = false"
151+
ref="searchBoxRef"
152+
:inputClass="isSearchExpanded ? 'w-full' : ''"
153+
:class="{ 'max-w-md': !isSearchExpanded }"
154+
@focus="handleSearchFocus"
155+
@blur="handleSearchBlur"
68156
/>
69157
<ul
70-
:class="{ 'hidden sm:flex': showFullSearch }"
71-
class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
158+
v-if="!isSearchExpanded"
159+
:class="{ hidden: showFullSearch }"
160+
class="hidden sm:flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
72161
>
73162
<!-- Packages dropdown (when connected) -->
74163
<li v-if="isConnected && npmUser" class="flex items-center">
@@ -82,34 +171,61 @@ onKeyStroke(
82171
</ul>
83172
</div>
84173

85-
<!-- End: User status + GitHub -->
86-
<div
87-
:class="{ 'hidden sm:flex': showFullSearch }"
88-
class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ms-auto sm:ms-0"
89-
>
174+
<!-- End: Desktop nav items + Mobile menu button -->
175+
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6">
176+
<!-- Desktop: Compare link -->
90177
<NuxtLink
91-
to="/about"
92-
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
178+
to="/compare"
179+
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
180+
aria-keyshortcuts="c"
93181
>
94-
{{ $t('footer.about') }}
182+
{{ $t('nav.compare') }}
183+
<kbd
184+
class="inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
185+
aria-hidden="true"
186+
>
187+
c
188+
</kbd>
95189
</NuxtLink>
96190

191+
<!-- Desktop: Settings link -->
97192
<NuxtLink
98193
to="/settings"
99-
class="link-subtle font-mono text-sm inline-flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
194+
class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
100195
aria-keyshortcuts=","
101196
>
102197
{{ $t('nav.settings') }}
103198
<kbd
104-
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
199+
class="inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
105200
aria-hidden="true"
106201
>
107202
,
108203
</kbd>
109204
</NuxtLink>
110205

111-
<HeaderAccountMenu />
206+
<!-- Desktop: Account menu -->
207+
<div class="hidden sm:block">
208+
<HeaderAccountMenu />
209+
</div>
210+
211+
<!-- Mobile: Menu button (always visible, toggles menu) -->
212+
<button
213+
type="button"
214+
class="sm:hidden flex items-center p-2 -m-2 text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
215+
:aria-label="showMobileMenu ? $t('common.close') : $t('nav.open_menu')"
216+
:aria-expanded="showMobileMenu"
217+
@click="showMobileMenu = !showMobileMenu"
218+
>
219+
<span
220+
class="w-6 h-6 inline-block"
221+
:class="showMobileMenu ? 'i-carbon:close' : 'i-carbon:menu'"
222+
aria-hidden="true"
223+
/>
224+
</button>
112225
</div>
113226
</nav>
227+
228+
<!-- Mobile menu -->
229+
<MobileMenu v-model:open="showMobileMenu" />
114230
</header>
115231
</template>

app/components/BuildEnvironment.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const buildInfo = useAppConfig().buildInfo
1313
:class="footer ? 'mt-4 justify-start' : 'mb-8 justify-center'"
1414
style="animation-delay: 0.05s"
1515
>
16-
<i18n-t keypath="built_at">
16+
<i18n-t keypath="built_at" scope="global">
1717
<NuxtTime :datetime="buildInfo.time" :locale="locale" relative />
1818
</i18n-t>
1919
<span>&middot;</span>

app/components/CollapsibleSection.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed } from 'vue'
2+
import { shallowRef, computed } from 'vue'
33
44
interface Props {
55
title: string
@@ -19,7 +19,7 @@ const buttonId = `${props.id}-collapsible-button`
1919
const contentId = `${props.id}-collapsible-content`
2020
const headingId = `${props.id}-heading`
2121
22-
const isOpen = ref(true)
22+
const isOpen = shallowRef(true)
2323
2424
onPrehydrate(() => {
2525
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
@@ -123,7 +123,7 @@ useHead({
123123
:id="contentId"
124124
class="grid ms-6 transition-[grid-template-rows] duration-200 ease-in-out collapsible-content overflow-hidden"
125125
>
126-
<div class="min-h-0">
126+
<div class="min-h-0 min-w-0">
127127
<slot />
128128
</div>
129129
</div>

0 commit comments

Comments
 (0)