Skip to content

Commit cf33ca9

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix-a11y-extract-btn-component
2 parents 80bcd90 + 2273d3b commit cf33ca9

36 files changed

Lines changed: 586 additions & 215 deletions

.github/workflows/ci.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ jobs:
3333

3434
- uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # 1e1c8eafbd745f64b1ef30a7d7ed7965034c486c
3535
name: 🟧 Install pnpm
36-
# pnpm cache skipped deliberately as the project is not actually installed here
36+
with:
37+
cache: true
38+
39+
- name: 📦 Install dependencies (root only, no scripts)
40+
run: pnpm install --filter . --ignore-scripts
3741

3842
- name: 🔠 Lint project
39-
run: node scripts/lint.ts
43+
run: pnpm lint
4044

4145
types:
4246
name: 💪 Type check

.oxlintrc.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"$schema": "https://unpkg.com/oxlint/configuration_schema.json",
33
"plugins": ["unicorn", "typescript", "oxc", "vue", "vitest"],
4+
"jsPlugins": ["@e18e/eslint-plugin"],
45
"categories": {
56
"correctness": "error",
67
"suspicious": "warn",
@@ -11,8 +12,27 @@
1112
"no-await-in-loop": "off",
1213
"unicorn/no-array-sort": "off",
1314
"no-restricted-globals": "error",
14-
"typescript/consistent-type-imports": "error"
15+
"typescript/consistent-type-imports": "error",
16+
"e18e/prefer-array-from-map": "error",
17+
"e18e/prefer-timer-args": "error",
18+
"e18e/prefer-date-now": "error",
19+
"e18e/prefer-regex-test": "error",
20+
"e18e/prefer-array-some": "error"
1521
},
22+
"overrides": [
23+
{
24+
"files": [
25+
"server/**/*",
26+
"cli/**/*",
27+
"scripts/**/*",
28+
"modules/**/*",
29+
"app/components/OgImage/*"
30+
],
31+
"rules": {
32+
"no-console": "off"
33+
}
34+
}
35+
],
1636
"ignorePatterns": [
1737
".output/**",
1838
".data/**",

CONTRIBUTING.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,12 +340,12 @@ To add a new locale:
340340
5. If the language is `right-to-left`, add `dir: 'rtl'` (see `ar-EG` in config for example)
341341
6. If the language requires special pluralization rules, add a `pluralRule` callback (see `ar-EG` or `ru-RU` in config for examples)
342342

343-
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization) for more info.
343+
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization#custom-pluralization) and [Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules#TOC-Determining-Plural-Categories) for more info.
344344

345345
### Update translation
346346

347347
We track the current progress of translations with [Lunaria](https://lunaria.dev/) on this site: https://i18n.npmx.dev/
348-
If you see any outdated translations in your language, feel free to update the keys to match then English version.
348+
If you see any outdated translations in your language, feel free to update the keys to match the English version.
349349

350350
In order to make sure you have everything up-to-date, you can run:
351351

@@ -408,13 +408,51 @@ See how `es`, `es-ES`, and `es-419` are configured in [config/i18n.ts](./config/
408408
<p>{{ $t('greeting', { name: userName }) }}</p>
409409
```
410410

411+
4. Don't concatenate string messages in the Vue templates, some languages can have different word order. Use placeholders instead.
412+
413+
**Bad:**
414+
415+
```vue
416+
<p>{{ $t('hello') }} {{ userName }}</p>
417+
```
418+
419+
**Good:**
420+
421+
```vue
422+
<p>{{ $t('greeting', { name: userName }) }}</p>
423+
```
424+
425+
**Complex content:**
426+
427+
If you need to include HTML or components inside the translation, use [`i18n-t`](https://vue-i18n.intlify.dev/guide/advanced/component.html) component. This is especially useful when the order of elements might change between languages.
428+
429+
```json
430+
{
431+
"agreement": "I accept the {terms} and {privacy}.",
432+
"terms_link": "Terms of Service",
433+
"privacy_policy": "Privacy Policy"
434+
}
435+
```
436+
437+
```vue
438+
<i18n-t keypath="agreement" tag="p">
439+
<template #terms>
440+
<NuxtLink to="/terms">{{ $t('terms_link') }}</NuxtLink>
441+
</template>
442+
<template #privacy>
443+
<strong>{{ $t('privacy_policy') }}</strong>
444+
</template>
445+
</i18n-t>
446+
```
447+
411448
### Translation key conventions
412449

413450
- Use dot notation for hierarchy: `section.subsection.key`
414451
- Keep keys descriptive but concise
415452
- Group related keys together
416453
- Use `common.*` for shared strings (loading, retry, close, etc.)
417454
- Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*`
455+
- Do not use dashes (`-`) in translation keys; always use underscore (`_`): e.g., `privacy_policy` instead of `privacy-policy`
418456

419457
### Using i18n-ally (recommended)
420458

app/app.vue

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import type { Directions } from '@nuxtjs/i18n'
3-
import { useEventListener } from '@vueuse/core'
3+
import { useEventListener, onKeyDown, onKeyUp } from '@vueuse/core'
44
import { isEditableElement } from '~/utils/input'
55
66
const route = useRoute()
@@ -47,16 +47,12 @@ if (import.meta.server) {
4747
setJsonLd(createWebSiteSchema())
4848
}
4949
50-
// Global keyboard shortcut:
51-
// "/" focuses search or navigates to search page
52-
// "?" highlights all keyboard shortcut elements
53-
function handleGlobalKeydown(e: KeyboardEvent) {
54-
if (isEditableElement(e.target)) return
55-
56-
if (isKeyWithoutModifiers(e, '/')) {
50+
onKeyDown(
51+
'/',
52+
e => {
53+
if (isEditableElement(e.target)) return
5754
e.preventDefault()
5855
59-
// Try to find and focus search input on current page
6056
const searchInput = document.querySelector<HTMLInputElement>(
6157
'input[type="search"], input[name="q"]',
6258
)
@@ -67,18 +63,29 @@ function handleGlobalKeydown(e: KeyboardEvent) {
6763
}
6864
6965
router.push('/search')
70-
}
66+
},
67+
{ dedupe: true },
68+
)
7169
72-
// For "?" we check the key property directly since it's usually combined with shift
73-
if (e.key === '?') {
70+
onKeyDown(
71+
'?',
72+
e => {
73+
if (isEditableElement(e.target)) return
7474
e.preventDefault()
7575
showKbdHints.value = true
76-
}
77-
}
76+
},
77+
{ dedupe: true },
78+
)
7879
79-
function handleGlobalKeyup() {
80-
showKbdHints.value = false
81-
}
80+
onKeyUp(
81+
'?',
82+
e => {
83+
if (isEditableElement(e.target)) return
84+
e.preventDefault()
85+
showKbdHints.value = false
86+
},
87+
{ dedupe: true },
88+
)
8289
8390
// Light dismiss fallback for browsers that don't support closedby="any" (Safari + old Chrome/Firefox)
8491
// https://codepen.io/paramagicdev/pen/gbYompq
@@ -99,9 +106,6 @@ function handleModalLightDismiss(e: MouseEvent) {
99106
}
100107
101108
if (import.meta.client) {
102-
useEventListener(document, 'keydown', handleGlobalKeydown)
103-
useEventListener(document, 'keyup', handleGlobalKeyup)
104-
105109
// Feature check for native light dismiss support via closedby="any"
106110
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
107111
const supportsClosedBy =

app/assets/main.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ html {
156156
text-rendering: optimizeLegibility;
157157
scroll-padding-top: 5rem;
158158
/* Offset for fixed header - otherwise anchor headers are cutted */
159+
scrollbar-gutter: stable;
159160
}
160161

161162
/*
@@ -192,6 +193,10 @@ dd {
192193
}
193194

194195
/* Scrollbar styling */
196+
* {
197+
scrollbar-color: var(--border) var(--bg);
198+
}
199+
195200
::-webkit-scrollbar {
196201
width: 8px;
197202
height: 8px;
@@ -207,14 +212,13 @@ dd {
207212
}
208213

209214
::-webkit-scrollbar-thumb:hover {
210-
background: #404040;
215+
background: var(--border-hover);
211216
}
212217

213218
/* Scrollbar styling for Firefox */
214219
@supports not selector(::-webkit-scrollbar) {
215220
* {
216221
scrollbar-width: thin;
217-
scrollbar-color: var(--border) var(--bg);
218222
}
219223
}
220224

app/components/ColumnPicker.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { ColumnConfig, ColumnId } from '#shared/types/preferences'
3+
import { onKeyDown } from '@vueuse/core'
34
45
const props = defineProps<{
56
columns: ColumnConfig[]
@@ -26,13 +27,16 @@ onClickOutside(
2627
},
2728
)
2829
29-
// Close on Escape key
30-
useEventListener('keydown', event => {
31-
if (event.key === 'Escape' && isOpen.value) {
30+
onKeyDown(
31+
'Escape',
32+
e => {
33+
if (!isOpen.value) return
3234
isOpen.value = false
3335
buttonRef.value?.focus()
34-
}
35-
})
36+
},
37+
{ dedupe: true },
38+
)
39+
3640
// Columns that can be toggled (name is always visible)
3741
const toggleableColumns = computed(() => props.columns.filter(col => col.id !== 'name'))
3842

app/components/DependencyPathPopup.vue

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<script setup lang="ts">
2+
import { onKeyDown } from '@vueuse/core'
3+
import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component'
4+
25
defineProps<{
36
/** Dependency path from root to vulnerable package (readonly from VulnerabilityTreeResult) */
47
path: readonly string[]
@@ -17,13 +20,16 @@ onClickOutside(popupEl, () => {
1720
if (isOpen.value) closePopup()
1821
})
1922
20-
// Close popup on ESC or scroll
21-
function handleKeydown(e: KeyboardEvent) {
22-
if (e.key === 'Escape') closePopup()
23-
}
23+
onKeyDown(
24+
'Escape',
25+
e => {
26+
e.preventDefault()
27+
closePopup()
28+
},
29+
{ dedupe: true, target: popupEl },
30+
)
2431
25-
useEventListener(document, 'keydown', handleKeydown)
26-
useEventListener('scroll', closePopup, true)
32+
useEventListener('scroll', closePopup, { passive: true })
2733
2834
function togglePopup(event: MouseEvent) {
2935
if (isOpen.value) {
@@ -77,34 +83,36 @@ function parsePackageString(pkg: string): { name: string; version: string } {
7783
class="fixed z-[100] bg-bg-elevated border border-border rounded-lg shadow-xl p-3 min-w-64 max-w-sm"
7884
:style="getPopupStyle()"
7985
>
80-
<ul class="list-none m-0 p-0 space-y-0.5">
81-
<li
82-
v-for="(pathItem, idx) in path"
83-
:key="idx"
84-
class="font-mono text-xs"
85-
:style="{ paddingLeft: `${idx * 12}px` }"
86-
>
87-
<span v-if="idx > 0" class="text-fg-subtle me-1">└─</span>
88-
<NuxtLink
89-
:to="{
90-
name: 'package',
91-
params: {
92-
package: [
93-
...parsePackageString(pathItem).name.split('/'),
94-
'v',
95-
parsePackageString(pathItem).version,
96-
],
97-
},
98-
}"
99-
class="hover:underline"
100-
:class="idx === path.length - 1 ? 'text-fg font-medium' : 'text-fg-muted'"
101-
@click="closePopup"
86+
<UseFocusTrap :options="{ immediate: true }">
87+
<ul class="list-none m-0 p-0 space-y-0.5">
88+
<li
89+
v-for="(pathItem, idx) in path"
90+
:key="idx"
91+
class="font-mono text-xs"
92+
:style="{ paddingLeft: `${idx * 12}px` }"
10293
>
103-
{{ pathItem }}
104-
</NuxtLink>
105-
<span v-if="idx === path.length - 1" class="ms-1 text-amber-500">⚠</span>
106-
</li>
107-
</ul>
94+
<span v-if="idx > 0" class="text-fg-subtle me-1">└─</span>
95+
<NuxtLink
96+
:to="{
97+
name: 'package',
98+
params: {
99+
package: [
100+
...parsePackageString(pathItem).name.split('/'),
101+
'v',
102+
parsePackageString(pathItem).version,
103+
],
104+
},
105+
}"
106+
class="hover:underline"
107+
:class="idx === path.length - 1 ? 'text-fg font-medium' : 'text-fg-muted'"
108+
@click="closePopup"
109+
>
110+
{{ pathItem }}
111+
</NuxtLink>
112+
<span v-if="idx === path.length - 1" class="ms-1 text-amber-500">⚠</span>
113+
</li>
114+
</ul>
115+
</UseFocusTrap>
108116
</div>
109117
</div>
110118
</template>

app/components/Header/AccountMenu.client.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function openAuthModal() {
7777
:alt="npmUser || $t('account_menu.npm_cli')"
7878
width="24"
7979
height="24"
80-
class="w-6 h-6 rounded-full ring-2 ring-bg"
80+
class="w-6 h-6 rounded-full ring-2 ring-bg object-cover"
8181
/>
8282
<span
8383
v-else-if="isNpmConnected"
@@ -93,7 +93,7 @@ function openAuthModal() {
9393
:alt="atprotoUser.handle"
9494
width="24"
9595
height="24"
96-
class="w-6 h-6 rounded-full ring-2 ring-bg"
96+
class="w-6 h-6 rounded-full ring-2 ring-bg object-cover"
9797
:class="hasBothConnections ? 'relative z-10' : ''"
9898
/>
9999
<span
@@ -153,7 +153,7 @@ function openAuthModal() {
153153
:alt="npmUser"
154154
width="32"
155155
height="32"
156-
class="w-8 h-8 rounded-full"
156+
class="w-8 h-8 rounded-full object-cover"
157157
/>
158158
<span
159159
v-else
@@ -196,7 +196,7 @@ function openAuthModal() {
196196
:alt="atprotoUser.handle"
197197
width="32"
198198
height="32"
199-
class="w-8 h-8 rounded-full"
199+
class="w-8 h-8 rounded-full object-cover"
200200
/>
201201
<span
202202
v-else

0 commit comments

Comments
 (0)