Skip to content

Commit 05b887f

Browse files
committed
merge main
2 parents 2725f18 + be007fe commit 05b887f

File tree

215 files changed

+17176
-3273
lines changed

Some content is hidden

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

215 files changed

+17176
-3273
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,8 @@ jobs:
235235

236236
- name: 🌐 Check for missing or dynamic i18n keys
237237
run: pnpm i18n:report
238+
239+
- name: 🌐 Check i18n schema is up to date
240+
run: |
241+
pnpm i18n:schema
242+
git diff --exit-code i18n/schema.json
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: mirror
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- '*'
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
mirror:
15+
name: 🕸️ Mirror to Tangled
16+
runs-on: ubuntu-24.04-arm
17+
18+
steps:
19+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
20+
with:
21+
fetch-depth: 0
22+
23+
- name: 🔑 Configure SSH
24+
env:
25+
TANGLED_SSH_KEY: ${{ secrets.TANGLED_SSH_KEY }}
26+
run: |
27+
mkdir -p ~/.ssh
28+
echo "$TANGLED_SSH_KEY" > ~/.ssh/id_ed25519
29+
chmod 600 ~/.ssh/id_ed25519
30+
ssh-keyscan -t ed25519 tangled.org >> ~/.ssh/known_hosts 2>/dev/null
31+
32+
- name: ⬆︎ Push to Tangled
33+
run: |
34+
git remote add tangled git@tangled.org:npmx.dev/npmx.dev
35+
git push tangled main --force
36+
git push tangled --tags --force

.lighthouserc.cjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,18 @@ module.exports = {
3535
chromePath: findChrome(),
3636
puppeteerScript: './lighthouse-setup.cjs',
3737
settings: {
38-
onlyCategories: ['accessibility'],
38+
onlyCategories: process.env.LH_PERF ? ['performance'] : ['accessibility'],
3939
skipAudits: ['valid-source-maps'],
4040
},
4141
},
4242
assert: {
43-
assertions: {
44-
'categories:accessibility': ['error', { minScore: 1 }],
45-
},
43+
assertions: process.env.LH_PERF
44+
? {
45+
'cumulative-layout-shift': ['error', { maxNumericValue: 0 }],
46+
}
47+
: {
48+
'categories:accessibility': ['error', { minScore: 1 }],
49+
},
4650
},
4751
upload: {
4852
target: 'temporary-public-storage',

CONTRIBUTING.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ This focus helps guide our project decisions as a community and what we choose t
5454
- [Unit tests](#unit-tests)
5555
- [Component accessibility tests](#component-accessibility-tests)
5656
- [Lighthouse accessibility tests](#lighthouse-accessibility-tests)
57+
- [Lighthouse performance tests](#lighthouse-performance-tests)
5758
- [End to end tests](#end-to-end-tests)
5859
- [Test fixtures (mocking external APIs)](#test-fixtures-mocking-external-apis)
5960
- [Submitting changes](#submitting-changes)
@@ -114,6 +115,7 @@ pnpm test:unit # Unit tests only
114115
pnpm test:nuxt # Nuxt component tests
115116
pnpm test:browser # Playwright E2E tests
116117
pnpm test:a11y # Lighthouse accessibility audits
118+
pnpm test:perf # Lighthouse performance audits (CLS)
117119
```
118120

119121
### Project structure
@@ -641,18 +643,38 @@ pnpm test:a11y:prebuilt
641643

642644
# Or run a single color mode manually
643645
pnpm build:test
644-
LIGHTHOUSE_COLOR_MODE=dark ./scripts/lighthouse-a11y.sh
646+
LIGHTHOUSE_COLOR_MODE=dark ./scripts/lighthouse.sh
645647
```
646648

647649
This requires Chrome or Chromium to be installed. The script will auto-detect common installation paths. Results are printed to the terminal and saved in `.lighthouseci/`.
648650

649651
#### Configuration
650652

651-
| File | Purpose |
652-
| ---------------------------- | --------------------------------------------------------- |
653-
| `.lighthouserc.cjs` | Lighthouse CI config (URLs, assertions, Chrome path) |
654-
| `lighthouse-setup.cjs` | Puppeteer script for color mode + client-side API mocking |
655-
| `scripts/lighthouse-a11y.sh` | Shell wrapper that runs the audit for a given color mode |
653+
| File | Purpose |
654+
| ----------------------- | --------------------------------------------------------- |
655+
| `.lighthouserc.cjs` | Lighthouse CI config (URLs, assertions, Chrome path) |
656+
| `lighthouse-setup.cjs` | Puppeteer script for color mode + client-side API mocking |
657+
| `scripts/lighthouse.sh` | Shell wrapper that runs the audit for a given color mode |
658+
659+
### Lighthouse performance tests
660+
661+
The project also runs Lighthouse performance audits to enforce zero Cumulative Layout Shift (CLS). These run separately from the accessibility audits and test the same set of URLs.
662+
663+
#### How it works
664+
665+
The same `.lighthouserc.cjs` config is shared between accessibility and performance audits. When the `LH_PERF` environment variable is set, the config switches from the `accessibility` category to the `performance` category and asserts that CLS is exactly 0.
666+
667+
#### Running locally
668+
669+
```bash
670+
# Build + run performance audit
671+
pnpm test:perf
672+
673+
# Or against an existing test build
674+
pnpm test:perf:prebuilt
675+
```
676+
677+
Unlike the accessibility audits, performance audits do not run in separate light/dark modes.
656678

657679
### End to end tests
658680

@@ -767,8 +789,10 @@ Format: `type(scope): description`
767789
- `fix(i18n): update French translations`
768790
- `chore(deps): update vite to v6`
769791

792+
Where front end changes are made, please include before and after screenshots in your pull request description.
793+
770794
> [!NOTE]
771-
> The subject must start with a lowercase letter. Individual commit messages within your PR don't need to follow this format since they'll be squashed.
795+
> Use lowercase letters in your pull request title. Individual commit messages within your PR don't need to follow this format since they'll be squashed.
772796
773797
### PR descriptions
774798

app/components/AppFooter.vue

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const showModal = () => modalRef.value?.showModal?.()
1414
>
1515
<div>
1616
<p class="font-mono text-balance m-0 hidden sm:block">{{ $t('tagline') }}</p>
17-
<BuildEnvironment v-if="!isHome" footer />
1817
</div>
1918
<!-- Desktop: Show all links. Mobile: Links are in MobileMenu -->
2019
<div class="hidden sm:flex items-center gap-6 min-h-11 text-xs">
@@ -27,22 +26,9 @@ const showModal = () => modalRef.value?.showModal?.()
2726
<LinkBase :to="{ name: 'privacy' }">
2827
{{ $t('privacy_policy.title') }}
2928
</LinkBase>
30-
<LinkBase to="https://docs.npmx.dev">
31-
{{ $t('footer.docs') }}
32-
</LinkBase>
33-
<LinkBase to="https://repo.npmx.dev">
34-
{{ $t('footer.source') }}
35-
</LinkBase>
36-
<LinkBase to="https://social.npmx.dev">
37-
{{ $t('footer.social') }}
38-
</LinkBase>
39-
<LinkBase to="https://chat.npmx.dev">
40-
{{ $t('footer.chat') }}
41-
</LinkBase>
42-
4329
<button
4430
type="button"
45-
class="group inline-flex gap-x-1 items-center justify-center underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg hover:(decoration-accent text-accent) focus-visible:(decoration-accent text-accent) transition-colors duration-200"
31+
class="cursor-pointer group inline-flex gap-x-1 items-center justify-center underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg hover:(decoration-accent text-accent) focus-visible:(decoration-accent text-accent) transition-colors duration-200"
4632
@click.prevent="showModal"
4733
aria-haspopup="dialog"
4834
>
@@ -106,8 +92,21 @@ const showModal = () => modalRef.value?.showModal?.()
10692
</li>
10793
</ul>
10894
</Modal>
95+
<LinkBase to="https://docs.npmx.dev">
96+
{{ $t('footer.docs') }}
97+
</LinkBase>
98+
<LinkBase to="https://repo.npmx.dev">
99+
{{ $t('footer.source') }}
100+
</LinkBase>
101+
<LinkBase to="https://social.npmx.dev">
102+
{{ $t('footer.social') }}
103+
</LinkBase>
104+
<LinkBase to="https://chat.npmx.dev">
105+
{{ $t('footer.chat') }}
106+
</LinkBase>
109107
</div>
110108
</div>
109+
<BuildEnvironment v-if="!isHome" footer />
111110
<p class="text-xs text-fg-muted text-center sm:text-start m-0">
112111
<span class="sm:hidden">{{ $t('non_affiliation_disclaimer') }}</span>
113112
<span class="hidden sm:inline">{{ $t('trademark_disclaimer') }}</span>

app/components/AppHeader.vue

Lines changed: 127 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { LinkBase } from '#components'
3+
import type { NavigationConfig, NavigationConfigWithGroups } from '~/types'
34
import { isEditableElement } from '~/utils/input'
45
56
withDefaults(
@@ -13,6 +14,114 @@ withDefaults(
1314
1415
const { isConnected, npmUser } = useConnector()
1516
17+
const desktopLinks = computed<NavigationConfig>(() => [
18+
{
19+
name: 'Compare',
20+
label: $t('nav.compare'),
21+
to: { name: 'compare' },
22+
keyshortcut: 'c',
23+
type: 'link',
24+
external: false,
25+
iconClass: 'i-carbon:compare',
26+
},
27+
{
28+
name: 'Settings',
29+
label: $t('nav.settings'),
30+
to: { name: 'settings' },
31+
keyshortcut: ',',
32+
type: 'link',
33+
external: false,
34+
iconClass: 'i-carbon:settings',
35+
},
36+
])
37+
38+
const mobileLinks = computed<NavigationConfigWithGroups>(() => [
39+
{
40+
name: 'Desktop Links',
41+
type: 'group',
42+
items: [...desktopLinks.value],
43+
},
44+
{
45+
type: 'separator',
46+
},
47+
{
48+
name: 'About & Policies',
49+
type: 'group',
50+
items: [
51+
{
52+
name: 'About',
53+
label: $t('footer.about'),
54+
to: { name: 'about' },
55+
type: 'link',
56+
external: false,
57+
iconClass: 'i-carbon:information',
58+
},
59+
{
60+
name: 'Blog',
61+
label: $t('footer.blog'),
62+
to: { name: 'blog' },
63+
type: 'link',
64+
external: false,
65+
iconClass: 'i-carbon:blog',
66+
},
67+
{
68+
name: 'Privacy Policy',
69+
label: $t('privacy_policy.title'),
70+
to: { name: 'privacy' },
71+
type: 'link',
72+
external: false,
73+
iconClass: 'i-carbon:security',
74+
},
75+
],
76+
},
77+
{
78+
type: 'separator',
79+
},
80+
{
81+
name: 'External Links',
82+
type: 'group',
83+
label: $t('nav.links'),
84+
items: [
85+
{
86+
name: 'Docs',
87+
label: $t('footer.docs'),
88+
href: 'https://docs.npmx.dev',
89+
target: '_blank',
90+
type: 'link',
91+
external: true,
92+
iconClass: 'i-carbon:document',
93+
},
94+
{
95+
name: 'Source',
96+
label: $t('footer.source'),
97+
href: 'https://repo.npmx.dev',
98+
target: '_blank',
99+
type: 'link',
100+
external: true,
101+
iconClass: 'i-carbon:logo-github',
102+
},
103+
{
104+
name: 'Social',
105+
label: $t('footer.social'),
106+
href: 'https://social.npmx.dev',
107+
target: '_blank',
108+
type: 'link',
109+
external: true,
110+
iconClass: 'i-simple-icons:bluesky',
111+
},
112+
{
113+
name: 'Chat',
114+
label: $t('footer.chat'),
115+
href: 'https://chat.npmx.dev',
116+
target: '_blank',
117+
type: 'link',
118+
external: true,
119+
iconClass: 'i-carbon:chat',
120+
},
121+
],
122+
},
123+
])
124+
16125
const showFullSearch = shallowRef(false)
17126
const showMobileMenu = shallowRef(false)
18127
@@ -63,23 +172,18 @@ function handleSearchFocus() {
63172
}
64173
65174
onKeyStroke(
66-
e => isKeyWithoutModifiers(e, ',') && !isEditableElement(e.target),
67175
e => {
68-
e.preventDefault()
69-
navigateTo({ name: 'settings' })
70-
},
71-
{ dedupe: true },
72-
)
176+
if (isEditableElement(e.target)) {
177+
return
178+
}
73179
74-
onKeyStroke(
75-
e =>
76-
isKeyWithoutModifiers(e, 'c') &&
77-
!isEditableElement(e.target) &&
78-
// Allow more specific handlers to take precedence
79-
!e.defaultPrevented,
80-
e => {
81-
e.preventDefault()
82-
navigateTo({ name: 'compare' })
180+
for (const link of desktopLinks.value) {
181+
if (link.to && link.keyshortcut && isKeyWithoutModifiers(e, link.keyshortcut)) {
182+
e.preventDefault()
183+
navigateTo(link.to.name)
184+
break
185+
}
186+
}
83187
},
84188
{ dedupe: true },
85189
)
@@ -156,24 +260,16 @@ onKeyStroke(
156260

157261
<!-- End: Desktop nav items + Mobile menu button -->
158262
<div class="hidden sm:flex flex-shrink-0">
159-
<!-- Desktop: Compare link -->
160-
<LinkBase
161-
class="border-none"
162-
variant="button-secondary"
163-
:to="{ name: 'compare' }"
164-
keyshortcut="c"
165-
>
166-
{{ $t('nav.compare') }}
167-
</LinkBase>
168-
169-
<!-- Desktop: Settings link -->
263+
<!-- Desktop: Explore link -->
170264
<LinkBase
265+
v-for="link in desktopLinks"
266+
:key="link.name"
171267
class="border-none"
172268
variant="button-secondary"
173-
:to="{ name: 'settings' }"
174-
keyshortcut=","
269+
:to="link.to"
270+
:aria-keyshortcuts="link.keyshortcut"
175271
>
176-
{{ $t('nav.settings') }}
272+
{{ link.label }}
177273
</LinkBase>
178274

179275
<HeaderAccountMenu />
@@ -182,7 +278,7 @@ onKeyStroke(
182278
<!-- Mobile: Menu button (always visible, click to open menu) -->
183279
<ButtonBase
184280
type="button"
185-
class="sm:hidden flex"
281+
class="sm:hidden"
186282
:aria-label="$t('nav.open_menu')"
187283
:aria-expanded="showMobileMenu"
188284
@click="showMobileMenu = !showMobileMenu"
@@ -191,6 +287,6 @@ onKeyStroke(
191287
</nav>
192288

193289
<!-- Mobile menu -->
194-
<HeaderMobileMenu v-model:open="showMobileMenu" />
290+
<HeaderMobileMenu :links="mobileLinks" v-model:open="showMobileMenu" />
195291
</header>
196292
</template>

0 commit comments

Comments
 (0)