Skip to content

Commit 7346552

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/skills-modal-tabs
2 parents 6b7cd7e + 5ec03c0 commit 7346552

9 files changed

Lines changed: 136 additions & 57 deletions

File tree

app/app.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ if (import.meta.server) {
4242
function handleGlobalKeydown(e: KeyboardEvent) {
4343
if (isEditableElement(e.target)) return
4444
45-
if (e.key === '/') {
45+
if (isKeyWithoutModifiers(e, '/')) {
4646
e.preventDefault()
4747
4848
// Try to find and focus search input on current page
@@ -58,7 +58,7 @@ function handleGlobalKeydown(e: KeyboardEvent) {
5858
router.push('/search')
5959
}
6060
61-
if (e.key === '?') {
61+
if (isKeyWithoutModifiers(e, '?')) {
6262
e.preventDefault()
6363
showKbdHints.value = true
6464
}

app/components/AppHeader.vue

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,23 +62,21 @@ function handleSearchFocus() {
6262
}
6363
6464
onKeyStroke(
65-
',',
65+
e => isKeyWithoutModifiers(e, ',') && !isEditableElement(e.target),
6666
e => {
67-
if (isEditableElement(e.target)) return
68-
6967
e.preventDefault()
7068
navigateTo('/settings')
7169
},
7270
{ dedupe: true },
7371
)
7472
7573
onKeyStroke(
76-
'c',
77-
e => {
74+
e =>
75+
isKeyWithoutModifiers(e, 'c') &&
76+
!isEditableElement(e.target) &&
7877
// Allow more specific handlers to take precedence
79-
if (e.defaultPrevented) return
80-
if (isEditableElement(e.target)) return
81-
78+
!e.defaultPrevented,
79+
e => {
8280
e.preventDefault()
8381
navigateTo('/compare')
8482
},

app/components/MobileMenu.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ const route = useRoute()
2929
watch(() => route.fullPath, closeMenu)
3030
3131
// Close on escape
32-
onKeyStroke('Escape', () => {
33-
if (isOpen.value) {
32+
onKeyStroke(
33+
e => isKeyWithoutModifiers(e, 'Escape') && isOpen.value,
34+
e => {
3435
isOpen.value = false
35-
}
36-
})
36+
},
37+
)
3738
3839
// Prevent body scroll when menu is open
3940
const isLocked = useScrollLock(document)

app/components/PackageDownloadAnalytics.vue

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,16 @@ const rootEl = shallowRef<HTMLElement | null>(null)
1919
2020
const { width } = useElementSize(rootEl)
2121
22-
onMounted(() => {
22+
const chartKey = ref(0)
23+
24+
onMounted(async () => {
2325
rootEl.value = document.documentElement
2426
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
27+
requestAnimationFrame(() => {
28+
requestAnimationFrame(() => {
29+
chartKey.value += 1
30+
})
31+
})
2532
})
2633
2734
const { colors } = useCssVariables(
@@ -543,6 +550,7 @@ const config = computed(() => {
543550
show: false, // As long as a single package is displayed
544551
},
545552
tooltip: {
553+
teleportTo: '#chart-modal',
546554
borderColor: 'transparent',
547555
backdropFilter: false,
548556
backgroundColor: 'transparent',
@@ -687,7 +695,12 @@ const config = computed(() => {
687695
</div>
688696

689697
<ClientOnly v-if="inModal && chartData.dataset">
690-
<VueUiXy :dataset="chartData.dataset" :config="config" class="[direction:ltr]">
698+
<VueUiXy
699+
:dataset="chartData.dataset"
700+
:config="config"
701+
class="[direction:ltr]"
702+
:key="chartKey"
703+
>
691704
<template #menuIcon="{ isOpen }">
692705
<span v-if="isOpen" class="i-carbon:close w-6 h-6" aria-hidden="true" />
693706
<span v-else class="i-carbon:overflow-menu-vertical w-6 h-6" aria-hidden="true" />

app/pages/[...package].vue

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -327,41 +327,38 @@ useSeoMeta({
327327
})
328328
329329
onKeyStroke(
330-
'.',
330+
e => isKeyWithoutModifiers(e, '.') && !isEditableElement(e.target),
331331
e => {
332-
if (isEditableElement(e.target)) return
333-
if (pkg.value && displayVersion.value) {
334-
e.preventDefault()
335-
navigateTo({
336-
name: 'code',
337-
params: {
338-
path: [pkg.value.name, 'v', displayVersion.value.version],
339-
},
340-
})
341-
}
332+
if (pkg.value == null || displayVersion.value == null) return
333+
e.preventDefault()
334+
navigateTo({
335+
name: 'code',
336+
params: {
337+
path: [pkg.value.name, 'v', displayVersion.value.version],
338+
},
339+
})
342340
},
343341
{ dedupe: true },
344342
)
345343
346344
onKeyStroke(
347-
'd',
345+
e => isKeyWithoutModifiers(e, 'd') && !isEditableElement(e.target),
348346
e => {
349-
if (isEditableElement(e.target)) return
350-
if (docsLink.value) {
351-
e.preventDefault()
352-
navigateTo(docsLink.value)
353-
}
347+
if (!docsLink.value) return
348+
e.preventDefault()
349+
navigateTo(docsLink.value)
354350
},
355351
{ dedupe: true },
356352
)
357353
358-
onKeyStroke('c', e => {
359-
if (isEditableElement(e.target)) return
360-
if (pkg.value) {
354+
onKeyStroke(
355+
e => isKeyWithoutModifiers(e, 'c') && !isEditableElement(e.target),
356+
e => {
357+
if (!pkg.value) return
361358
e.preventDefault()
362359
router.push({ path: '/compare', query: { packages: pkg.value.name } })
363-
}
364-
})
360+
},
361+
)
365362
366363
defineOgImageComponent('Package', {
367364
name: () => pkg.value?.name ?? 'Package',
@@ -392,7 +389,7 @@ function handleClick(event: MouseEvent) {
392389
</script>
393390

394391
<template>
395-
<main class="container flex-1 py-8 xl:py-12">
392+
<main class="container flex-1 w-full py-8 xl:py-12">
396393
<PackageSkeleton v-if="status === 'pending'" />
397394

398395
<article v-else-if="status === 'success' && pkg" class="package-page">

app/pages/settings.vue

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,15 @@ const { locale, locales, setLocale: setNuxti18nLocale } = useI18n()
55
const colorMode = useColorMode()
66
const { currentLocaleStatus, isSourceLocale } = useI18nStatus()
77
8-
// Escape to go back (but not when focused on form elements)
8+
// Escape to go back (but not when focused on form elements or modal is open)
99
onKeyStroke(
10-
'Escape',
10+
e =>
11+
isKeyWithoutModifiers(e, 'Escape') &&
12+
!isEditableElement(e.target) &&
13+
!document.documentElement.matches('html:has(:modal)'),
1114
e => {
12-
const target = e.target as HTMLElement
13-
if (
14-
!['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.tagName) &&
15-
!document.documentElement.matches('html:has(:modal)')
16-
) {
17-
e.preventDefault()
18-
router.back()
19-
}
15+
e.preventDefault()
16+
router.back()
2017
},
2118
{ dedupe: true },
2219
)
@@ -85,7 +82,9 @@ const setLocale: typeof setNuxti18nLocale = locale => {
8582
| 'system'
8683
"
8784
>
88-
<option value="system">{{ $t('settings.theme_system') }}</option>
85+
<option value="system">
86+
{{ $t('settings.theme_system') }}
87+
</option>
8988
<option value="light">{{ $t('settings.theme_light') }}</option>
9089
<option value="dark">{{ $t('settings.theme_dark') }}</option>
9190
</select>

app/utils/input.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,16 @@ export function isEditableElement(target: EventTarget | null): boolean {
1313
if (!target || !(target instanceof HTMLElement)) return false
1414
return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
1515
}
16+
17+
/**
18+
* Check if a keyboard event matches a specific key without any modifier keys.
19+
*/
20+
export function isKeyWithoutModifiers(event: KeyboardEvent, key: string): boolean {
21+
return (
22+
event.key.toLowerCase() === key.toLowerCase() &&
23+
!event.altKey &&
24+
!event.ctrlKey &&
25+
!event.metaKey &&
26+
!event.shiftKey
27+
)
28+
}

nuxt.config.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export default defineNuxtConfig({
3939

4040
css: ['~/assets/main.css', 'vue-data-ui/style.css'],
4141

42+
$production: {
43+
debug: {
44+
hydration: true,
45+
},
46+
},
47+
4248
runtimeConfig: {
4349
sessionPassword: '',
4450
// Upstash Redis for distributed OAuth token refresh locking in production
@@ -113,7 +119,7 @@ export default defineNuxtConfig({
113119
typedPages: true,
114120
},
115121

116-
compatibilityDate: '2024-04-03',
122+
compatibilityDate: '2026-01-31',
117123

118124
nitro: {
119125
experimental: {
@@ -219,10 +225,6 @@ export default defineNuxtConfig({
219225
},
220226
},
221227

222-
debug: {
223-
hydration: true,
224-
},
225-
226228
typescript: {
227229
tsConfig: {
228230
compilerOptions: {

test/e2e/interactions.spec.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ test.describe('Search Pages', () => {
4848
// Wait for navigation to /search (debounce is 250ms)
4949
await expect(page).toHaveURL(/\/search/, { timeout: 10000 })
5050

51-
await expect(page.locator('[data-result-index="0"]').first()).toBeVisible({ timeout: 15000 })
51+
await expect(page.locator('[data-result-index="0"]').first()).toBeVisible({
52+
timeout: 15000,
53+
})
5254

5355
// Home search input should be gone (we're on /search now)
5456
await expect(homeSearchInput).not.toBeVisible()
@@ -70,7 +72,9 @@ test.describe('Search Pages', () => {
7072

7173
await expect(page).toHaveURL(/\/search/, { timeout: 10000 })
7274

73-
await expect(page.locator('[data-result-index="0"]').first()).toBeVisible({ timeout: 15000 })
75+
await expect(page.locator('[data-result-index="0"]').first()).toBeVisible({
76+
timeout: 15000,
77+
})
7478

7579
const headerSearchInput = page.locator('#header-search')
7680
await expect(headerSearchInput).toBeFocused()
@@ -86,6 +90,21 @@ test.describe('Keyboard Shortcuts', () => {
8690
await expect(page).toHaveURL(/\/compare/)
8791
})
8892

93+
test('"c" does not navigate when any modifier key is pressed', async ({ page, goto }) => {
94+
await goto('/settings', { waitUntil: 'hydration' })
95+
96+
await page.keyboard.press('Shift+c')
97+
await expect(page).toHaveURL(/\/settings/)
98+
await page.keyboard.press('Control+c')
99+
await expect(page).toHaveURL(/\/settings/)
100+
await page.keyboard.press('Alt+c')
101+
await expect(page).toHaveURL(/\/settings/)
102+
await page.keyboard.press('Meta+c')
103+
await expect(page).toHaveURL(/\/settings/)
104+
await page.keyboard.press('ControlOrMeta+Shift+c')
105+
await expect(page).toHaveURL(/\/settings/)
106+
})
107+
89108
test('"c" on package page navigates to /compare with package pre-filled', async ({
90109
page,
91110
goto,
@@ -113,6 +132,24 @@ test.describe('Keyboard Shortcuts', () => {
113132
await expect(searchInput).toHaveValue('c')
114133
})
115134

135+
test('"c" on package page does not navigate when any modifier key is pressed', async ({
136+
page,
137+
goto,
138+
}) => {
139+
await goto('/vue', { waitUntil: 'hydration' })
140+
141+
await page.keyboard.press('Shift+c')
142+
await expect(page).toHaveURL(/\/vue/)
143+
await page.keyboard.press('Control+c')
144+
await expect(page).toHaveURL(/\/vue/)
145+
await page.keyboard.press('Alt+c')
146+
await expect(page).toHaveURL(/\/vue/)
147+
await page.keyboard.press('Meta+c')
148+
await expect(page).toHaveURL(/\/vue/)
149+
await page.keyboard.press('ControlOrMeta+Shift+c')
150+
await expect(page).toHaveURL(/\/vue/)
151+
})
152+
116153
test('"," navigates to /settings', async ({ page, goto }) => {
117154
await goto('/compare', { waitUntil: 'hydration' })
118155

@@ -121,6 +158,25 @@ test.describe('Keyboard Shortcuts', () => {
121158
await expect(page).toHaveURL(/\/settings/)
122159
})
123160

161+
test('"," does not navigate when any modifier key is pressed', async ({ page, goto }) => {
162+
await goto('/settings', { waitUntil: 'hydration' })
163+
164+
const searchInput = page.locator('#header-search')
165+
await searchInput.focus()
166+
await expect(searchInput).toBeFocused()
167+
168+
await page.keyboard.press('Shift+,')
169+
await expect(page).toHaveURL(/\/settings/)
170+
await page.keyboard.press('Control+,')
171+
await expect(page).toHaveURL(/\/settings/)
172+
await page.keyboard.press('Alt+,')
173+
await expect(page).toHaveURL(/\/settings/)
174+
await page.keyboard.press('Meta+,')
175+
await expect(page).toHaveURL(/\/settings/)
176+
await page.keyboard.press('ControlOrMeta+Shift+,')
177+
await expect(page).toHaveURL(/\/settings/)
178+
})
179+
124180
test('"," does not navigate when search input is focused', async ({ page, goto }) => {
125181
await goto('/compare', { waitUntil: 'hydration' })
126182

0 commit comments

Comments
 (0)