From bff7be6da48950c3086613223d6d7927c10ba09c Mon Sep 17 00:00:00 2001 From: Alex Korytskyi Date: Thu, 26 Feb 2026 21:21:06 +0000 Subject: [PATCH 01/11] feat: add keyboard shortcut toggle settings --- app/app.vue | 8 +++++--- app/components/AppFooter.vue | 16 +++++++++++++++- app/components/Button/Base.vue | 21 +++++++++++++-------- app/components/Compare/PackageSelector.vue | 2 ++ app/components/Link/Base.vue | 19 +++++++++++-------- app/composables/useSettings.ts | 12 ++++++++++++ app/pages/package/[[org]]/[name].vue | 8 +++++--- app/pages/settings.vue | 15 +++++++++++++++ i18n/locales/en.json | 11 ++++++++--- i18n/schema.json | 15 +++++++++++++++ lunaria/files/en-GB.json | 11 ++++++++--- lunaria/files/en-US.json | 11 ++++++++--- 12 files changed, 117 insertions(+), 32 deletions(-) diff --git a/app/app.vue b/app/app.vue index 1ea249300..af500571e 100644 --- a/app/app.vue +++ b/app/app.vue @@ -47,10 +47,12 @@ if (import.meta.server) { setJsonLd(createWebSiteSchema()) } +const keyboardShortcuts = useKeyboardShortcuts() + onKeyDown( '/', e => { - if (isEditableElement(e.target)) return + if (!keyboardShortcuts.value || isEditableElement(e.target)) return e.preventDefault() const searchInput = document.querySelector( @@ -70,7 +72,7 @@ onKeyDown( onKeyDown( '?', e => { - if (isEditableElement(e.target)) return + if (!keyboardShortcuts.value || isEditableElement(e.target)) return e.preventDefault() showKbdHints.value = true }, @@ -80,7 +82,7 @@ onKeyDown( onKeyUp( '?', e => { - if (isEditableElement(e.target)) return + if (!keyboardShortcuts.value || isEditableElement(e.target)) return e.preventDefault() showKbdHints.value = false }, diff --git a/app/components/AppFooter.vue b/app/components/AppFooter.vue index ad0c82daf..cea39c29f 100644 --- a/app/components/AppFooter.vue +++ b/app/components/AppFooter.vue @@ -6,6 +6,7 @@ const isHome = computed(() => route.name === 'index') const modalRef = useTemplateRef('modalRef') const showModal = () => modalRef.value?.showModal?.() +const closeModal = () => modalRef.value?.close?.() + + +
+

+ {{ $t('settings.sections.keyboard_shortcuts') }} +

+
+ +
+
diff --git a/i18n/locales/en.json b/i18n/locales/en.json index b7a14fe3e..323e6e61f 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -33,7 +33,9 @@ "navigate_results": "Navigate results", "go_to_result": "Go to result", "open_code_view": "Open code view", - "open_docs": "Open docs" + "open_docs": "Open docs", + "disable_shortcuts": "Disable shortcuts", + "disable_shortcuts_description": "You could disable keyboard shortcuts in {settings}." }, "search": { "label": "Search npm packages", @@ -84,7 +86,8 @@ "appearance": "Appearance", "display": "Display", "search": "Data source", - "language": "Language" + "language": "Language", + "keyboard_shortcuts": "Keyboard shortcuts" }, "data_source": { "label": "Data source", @@ -108,7 +111,9 @@ "accent_colors": "Accent colors", "clear_accent": "Clear accent color", "translation_progress": "Translation progress", - "background_themes": "Background shade" + "background_themes": "Background shade", + "keyboard_shortcuts_enabled": "Enable keyboard shortcuts", + "keyboard_shortcuts_enabled_description": "Keyboard shortcuts can be disabled if they conflict with other browser or system shortcuts" }, "i18n": { "missing_keys": "{count} missing translation | {count} missing translations", diff --git a/i18n/schema.json b/i18n/schema.json index ce915ed5e..39802a42b 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -105,6 +105,12 @@ }, "open_docs": { "type": "string" + }, + "disable_shortcuts": { + "type": "string" + }, + "disable_shortcuts_description": { + "type": "string" } }, "additionalProperties": false @@ -258,6 +264,9 @@ }, "language": { "type": "string" + }, + "keyboard_shortcuts": { + "type": "string" } }, "additionalProperties": false @@ -330,6 +339,12 @@ }, "background_themes": { "type": "string" + }, + "keyboard_shortcuts_enabled": { + "type": "string" + }, + "keyboard_shortcuts_enabled_description": { + "type": "string" } }, "additionalProperties": false diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json index 12789651b..9333f7577 100644 --- a/lunaria/files/en-GB.json +++ b/lunaria/files/en-GB.json @@ -32,7 +32,9 @@ "navigate_results": "Navigate results", "go_to_result": "Go to result", "open_code_view": "Open code view", - "open_docs": "Open docs" + "open_docs": "Open docs", + "disable_shortcuts": "Disable shortcuts", + "disable_shortcuts_description": "You could disable keyboard shortcuts in {settings}." }, "search": { "label": "Search npm packages", @@ -83,7 +85,8 @@ "appearance": "Appearance", "display": "Display", "search": "Data source", - "language": "Language" + "language": "Language", + "keyboard_shortcuts": "Keyboard shortcuts" }, "data_source": { "label": "Data source", @@ -107,7 +110,9 @@ "accent_colors": "Accent colors", "clear_accent": "Clear accent colour", "translation_progress": "Translation progress", - "background_themes": "Background shade" + "background_themes": "Background shade", + "keyboard_shortcuts_enabled": "Enable keyboard shortcuts", + "keyboard_shortcuts_enabled_description": "Keyboard shortcuts can be disabled if they conflict with other browser or system shortcuts" }, "i18n": { "missing_keys": "{count} missing translation | {count} missing translations", diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json index 7b621707e..49839a53f 100644 --- a/lunaria/files/en-US.json +++ b/lunaria/files/en-US.json @@ -32,7 +32,9 @@ "navigate_results": "Navigate results", "go_to_result": "Go to result", "open_code_view": "Open code view", - "open_docs": "Open docs" + "open_docs": "Open docs", + "disable_shortcuts": "Disable shortcuts", + "disable_shortcuts_description": "You could disable keyboard shortcuts in {settings}." }, "search": { "label": "Search npm packages", @@ -83,7 +85,8 @@ "appearance": "Appearance", "display": "Display", "search": "Data source", - "language": "Language" + "language": "Language", + "keyboard_shortcuts": "Keyboard shortcuts" }, "data_source": { "label": "Data source", @@ -107,7 +110,9 @@ "accent_colors": "Accent colors", "clear_accent": "Clear accent color", "translation_progress": "Translation progress", - "background_themes": "Background shade" + "background_themes": "Background shade", + "keyboard_shortcuts_enabled": "Enable keyboard shortcuts", + "keyboard_shortcuts_enabled_description": "Keyboard shortcuts can be disabled if they conflict with other browser or system shortcuts" }, "i18n": { "missing_keys": "{count} missing translation | {count} missing translations", From 5a9bc4a0739d3d2ce0d6dcd3de23966da8225447 Mon Sep 17 00:00:00 2001 From: Alex Korytskyi Date: Fri, 27 Feb 2026 03:26:34 +0000 Subject: [PATCH 02/11] feat: add e2e and unit for kbd shortcuts disabled --- app/components/AppHeader.vue | 4 +- test/e2e/interactions.spec.ts | 49 +++++++++++++--- test/nuxt/composables/use-settings.spec.ts | 66 ++++++++++++++++++++++ 3 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 test/nuxt/composables/use-settings.spec.ts diff --git a/app/components/AppHeader.vue b/app/components/AppHeader.vue index a016389c5..feb5d3341 100644 --- a/app/components/AppHeader.vue +++ b/app/components/AppHeader.vue @@ -4,6 +4,8 @@ import type { NavigationConfig, NavigationConfigWithGroups } from '~/types' import { isEditableElement } from '~/utils/input' import { NPMX_DOCS_SITE } from '#shared/utils/constants' +const keyboardShortcuts = useKeyboardShortcuts() + withDefaults( defineProps<{ showLogo?: boolean @@ -175,7 +177,7 @@ function handleSearchFocus() { onKeyStroke( e => { - if (isEditableElement(e.target)) { + if (!keyboardShortcuts.value || isEditableElement(e.target)) { return } diff --git a/test/e2e/interactions.spec.ts b/test/e2e/interactions.spec.ts index b900bbafa..330b1e3f2 100644 --- a/test/e2e/interactions.spec.ts +++ b/test/e2e/interactions.spec.ts @@ -223,19 +223,52 @@ test.describe('Keyboard Shortcuts', () => { await page.keyboard.press('ControlOrMeta+Shift+,') await expect(page).toHaveURL(/\/settings/) }) +}) - test('"," does not navigate when search input is focused', async ({ page, goto }) => { - await goto('/compare', { waitUntil: 'hydration' }) +test.describe('Keyboard Shortcuts disabled', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem('npmx-settings', JSON.stringify({ keyboardShortcuts: false })) + }) + }) - const searchInput = page.locator('#header-search') - await searchInput.focus() - await expect(searchInput).toBeFocused() + test('"," (header) does not navigate to /settings when shortcuts are disabled', async ({ + page, + goto, + }) => { + await goto('/compare', { waitUntil: 'hydration' }) await page.keyboard.press(',') - // Should still be on compare, not navigated to settings await expect(page).toHaveURL(/\/compare/) - // The ',' should have been typed into the input - await expect(searchInput).toHaveValue(',') + }) + + test('"/" (global) does not focus search input when shortcuts are disabled', async ({ + page, + goto, + }) => { + await goto('/search?q=vue', { waitUntil: 'hydration' }) + + await expect(page.locator('text=/found \\d+|showing \\d+/i').first()).toBeVisible({ + timeout: 15000, + }) + + // Focus a non-input element so "/" would normally steal focus to search + await page.locator('[data-result-index="0"]').first().focus() + + await page.keyboard.press('/') + + await expect(page.locator('input[type="search"]')).not.toBeFocused() + }) + + test('"d" (package) does not navigate to docs when shortcuts are disabled', async ({ + page, + goto, + }) => { + await goto('/package/vue', { waitUntil: 'hydration' }) + + await page.keyboard.press('d') + + await expect(page).toHaveURL(/\/package\/vue$/) }) }) diff --git a/test/nuxt/composables/use-settings.spec.ts b/test/nuxt/composables/use-settings.spec.ts new file mode 100644 index 000000000..abd5ad26d --- /dev/null +++ b/test/nuxt/composables/use-settings.spec.ts @@ -0,0 +1,66 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('useSettings - keyboardShortcuts', () => { + beforeEach(() => { + localStorage.clear() + }) + + afterEach(() => { + // Reset the singleton so the next test gets a fresh instance + localStorage.clear() + }) + + describe('default value', () => { + it('should default keyboardShortcuts to true', () => { + const { settings } = useSettings() + expect(settings.value.keyboardShortcuts).toBe(true) + }) + }) + + describe('useKeyboardShortcuts composable', () => { + it('should return true by default', () => { + const enabled = useKeyboardShortcuts() + expect(enabled.value).toBe(true) + }) + + it('should reflect changes made via settings', () => { + const { settings } = useSettings() + const enabled = useKeyboardShortcuts() + + settings.value.keyboardShortcuts = false + expect(enabled.value).toBe(false) + + settings.value.keyboardShortcuts = true + expect(enabled.value).toBe(true) + }) + + it('should be reactive', () => { + const { settings } = useSettings() + const enabled = useKeyboardShortcuts() + + expect(enabled.value).toBe(true) + + settings.value.keyboardShortcuts = false + expect(enabled.value).toBe(false) + }) + }) + + describe('persistence', () => { + it('should persist keyboardShortcuts=false to localStorage', () => { + const { settings } = useSettings() + settings.value.keyboardShortcuts = false + + const stored = JSON.parse(localStorage.getItem('npmx-settings') ?? '{}') + expect(stored.keyboardShortcuts).toBe(false) + }) + + it('should persist keyboardShortcuts=true to localStorage', () => { + const { settings } = useSettings() + settings.value.keyboardShortcuts = false + settings.value.keyboardShortcuts = true + + const stored = JSON.parse(localStorage.getItem('npmx-settings') ?? '{}') + expect(stored.keyboardShortcuts).toBe(true) + }) + }) +}) From e3f7315688c0cad28d3ae07fa3622fb1441d8157 Mon Sep 17 00:00:00 2001 From: Alex Korytskyi Date: Fri, 27 Feb 2026 05:32:59 +0000 Subject: [PATCH 03/11] fix: improve useKeyboardShortcuts usage, fix i18n keys --- app/components/AppFooter.vue | 2 +- app/components/Compare/PackageSelector.vue | 4 +++- app/components/Link/Base.vue | 3 ++- i18n/locales/en.json | 3 +-- i18n/schema.json | 3 --- lunaria/files/en-GB.json | 3 +-- lunaria/files/en-US.json | 3 +-- 7 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/components/AppFooter.vue b/app/components/AppFooter.vue index cea39c29f..ebb4ca174 100644 --- a/app/components/AppFooter.vue +++ b/app/components/AppFooter.vue @@ -97,7 +97,7 @@ const closeModal = () => modalRef.value?.close?.()

- +