Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>(
Expand All @@ -70,7 +72,7 @@ onKeyDown(
onKeyDown(
'?',
e => {
if (isEditableElement(e.target)) return
if (!keyboardShortcuts.value || isEditableElement(e.target)) return
e.preventDefault()
showKbdHints.value = true
},
Expand All @@ -80,7 +82,7 @@ onKeyDown(
onKeyUp(
'?',
e => {
if (isEditableElement(e.target)) return
if (!keyboardShortcuts.value || isEditableElement(e.target)) return
e.preventDefault()
showKbdHints.value = false
},
Expand Down
5 changes: 5 additions & 0 deletions app/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ input[type='search']::-webkit-search-results-decoration {
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

/* Hide keyboard shortcut hints before hydration when user disabled them */
:root[data-kbd-shortcuts='false'] [data-kbd-hint] {
display: none;
}

/* Locking the scroll whenever any of the modals are open */
html:has(dialog:modal) {
overflow: hidden;
Expand Down
16 changes: 15 additions & 1 deletion app/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const isHome = computed(() => route.name === 'index')
const modalRef = useTemplateRef('modalRef')
const showModal = () => modalRef.value?.showModal?.()
const closeModal = () => modalRef.value?.close?.()
</script>

<template>
Expand Down Expand Up @@ -81,7 +82,7 @@ const showModal = () => modalRef.value?.showModal?.()
<p class="mb-2 font-mono text-fg-subtle">
{{ $t('shortcuts.section.package') }}
</p>
<ul class="mb-6 flex flex-col gap-2">
<ul class="mb-8 flex flex-col gap-2">
<li class="flex gap-2 items-center">
<kbd class="kbd">.</kbd>
<span>{{ $t('shortcuts.open_code_view') }}</span>
Expand All @@ -95,6 +96,19 @@ const showModal = () => modalRef.value?.showModal?.()
<span>{{ $t('shortcuts.compare_from_package') }}</span>
</li>
</ul>
<p class="text-fg-muted leading-relaxed">
<i18n-t keypath="shortcuts.disable_shortcuts" tag="span" scope="global">
<template #settings>
<NuxtLink
:to="{ name: 'settings' }"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Link should target the keyboard-shortcuts section directly.

Line 103 currently routes to the settings page root only. To meet the “direct to setting” objective, point this link to the keyboard shortcuts section anchor instead of the page top.

Suggested change
-                    :to="{ name: 'settings' }"
+                    :to="{ name: 'settings', hash: '#keyboard-shortcuts' }"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
:to="{ name: 'settings' }"
:to="{ name: 'settings', hash: '#keyboard-shortcuts' }"

class="hover:text-fg underline decoration-fg-subtle/50 hover:decoration-fg"
@click="closeModal"
>
{{ $t('settings.title') }}
</NuxtLink>
</template>
</i18n-t>
</p>
</Modal>
<LinkBase :to="NPMX_DOCS_SITE">
{{ $t('footer.docs') }}
Expand Down
4 changes: 3 additions & 1 deletion app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -175,7 +177,7 @@ function handleSearchFocus() {
onKeyStroke(
e => {
if (isEditableElement(e.target)) {
if (!keyboardShortcuts.value || isEditableElement(e.target)) {
return
}
Expand Down
7 changes: 5 additions & 2 deletions app/components/Button/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const props = withDefaults(
const el = useTemplateRef('el')
const keyboardShortcutsEnabled = useKeyboardShortcuts()
defineExpose({
focus: () => el.value?.focus(),
getBoundingClientRect: () => el.value?.getBoundingClientRect(),
Expand Down Expand Up @@ -56,12 +58,13 @@ defineExpose({
*/
disabled ? true : undefined
"
:aria-keyshortcuts="ariaKeyshortcuts"
:aria-keyshortcuts="keyboardShortcutsEnabled ? ariaKeyshortcuts : undefined"
>
<span v-if="classicon" class="size-[1em]" :class="classicon" aria-hidden="true" />
<slot />
<kbd
v-if="ariaKeyshortcuts"
v-if="keyboardShortcutsEnabled && ariaKeyshortcuts"
data-kbd-hint
class="ms-2 inline-flex items-center justify-center w-4 h-4 text-xs text-fg bg-bg-muted border border-border rounded no-underline"
aria-hidden="true"
>
Expand Down
6 changes: 4 additions & 2 deletions app/components/Link/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const isLink = computed(() => props.variant === 'link')
const isButton = computed(() => !isLink.value)
const isButtonSmall = computed(() => props.size === 'small' && !isLink.value)
const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
const keyboardShortcutsEnabled = useKeyboardShortcuts()
</script>

<template>
Expand Down Expand Up @@ -97,7 +98,7 @@ const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
variant === 'button-primary',
}"
:to="to"
:aria-keyshortcuts="ariaKeyshortcuts"
:aria-keyshortcuts="keyboardShortcutsEnabled ? ariaKeyshortcuts : undefined"
:target="isLinkExternal ? '_blank' : undefined"
>
<span v-if="classicon" class="size-[1em]" :class="classicon" aria-hidden="true" />
Expand All @@ -114,7 +115,8 @@ const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
aria-hidden="true"
/>
<kbd
v-if="ariaKeyshortcuts"
v-if="keyboardShortcutsEnabled && ariaKeyshortcuts"
data-kbd-hint
class="ms-2 inline-flex items-center justify-center size-4 text-xs text-fg bg-bg-muted border border-border rounded no-underline"
aria-hidden="true"
>
Expand Down
28 changes: 28 additions & 0 deletions app/composables/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface AppSettings {
selectedLocale: LocaleObject['code'] | null
/** Search provider for package search */
searchProvider: SearchProvider
/** Enable/disable keyboard shortcuts */
keyboardShortcuts: boolean
/** Connector preferences */
connector: {
/** Automatically open the web auth page in the browser */
Expand All @@ -53,6 +55,7 @@ const DEFAULT_SETTINGS: AppSettings = {
selectedLocale: null,
preferredBackgroundTheme: null,
searchProvider: import.meta.test ? 'npm' : 'algolia',
keyboardShortcuts: true,
connector: {
autoOpenURL: false,
},
Expand Down Expand Up @@ -97,6 +100,31 @@ export function useRelativeDates() {
return computed(() => settings.value.relativeDates)
}

/**
* Composable for accessing just the keyboard shortcuts setting.
* Useful for components that only need to read this specific setting.
*/
export const useKeyboardShortcuts = createSharedComposable(function useKeyboardShortcuts() {
const { settings } = useSettings()
const enabled = computed(() => settings.value.keyboardShortcuts)

if (import.meta.client) {
watch(
enabled,
value => {
if (value) {
delete document.documentElement.dataset.kbdShortcuts
} else {
document.documentElement.dataset.kbdShortcuts = 'false'
}
},
{ immediate: true },
)
}

return enabled
})

/**
* Composable for managing accent color.
*/
Expand Down
8 changes: 5 additions & 3 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -688,8 +688,10 @@ const codeLink = computed((): RouteLocationRaw | null => {
}
})

const keyboardShortcuts = useKeyboardShortcuts()

onKeyStroke(
e => isKeyWithoutModifiers(e, '.') && !isEditableElement(e.target),
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, '.') && !isEditableElement(e.target),
e => {
if (codeLink.value === null) return
e.preventDefault()
Expand All @@ -700,7 +702,7 @@ onKeyStroke(
)

onKeyStroke(
e => isKeyWithoutModifiers(e, 'd') && !isEditableElement(e.target),
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, 'd') && !isEditableElement(e.target),
e => {
if (!docsLink.value) return
e.preventDefault()
Expand All @@ -710,7 +712,7 @@ onKeyStroke(
)

onKeyStroke(
e => isKeyWithoutModifiers(e, 'c') && !isEditableElement(e.target),
e => keyboardShortcuts.value && isKeyWithoutModifiers(e, 'c') && !isEditableElement(e.target),
e => {
if (!pkg.value) return
e.preventDefault()
Expand Down
17 changes: 17 additions & 0 deletions app/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ const { settings } = useSettings()
const { locale, locales, setLocale: setNuxti18nLocale } = useI18n()
const colorMode = useColorMode()
const { currentLocaleStatus, isSourceLocale } = useI18nStatus()
const keyboardShortcutsEnabled = useKeyboardShortcuts()
// Escape to go back (but not when focused on form elements or modal is open)
onKeyStroke(
e =>
keyboardShortcutsEnabled.value &&
isKeyWithoutModifiers(e, 'Escape') &&
!isEditableElement(e.target) &&
!document.documentElement.matches('html:has(:modal)'),
Expand Down Expand Up @@ -205,6 +207,7 @@ const setLocale: typeof setNuxti18nLocale = locale => {
</div>
</section>

<!-- LANGUAGE Section -->
<section>
<h2 class="text-xs text-fg-muted uppercase tracking-wider mb-4">
{{ $t('settings.sections.language') }}
Expand Down Expand Up @@ -260,6 +263,20 @@ const setLocale: typeof setNuxti18nLocale = locale => {
</template>
</div>
</section>

<!-- KEYBOARD SHORTCUTS Section -->
<section>
<h2 class="text-xs text-fg-muted uppercase tracking-wider mb-4">
{{ $t('settings.sections.keyboard_shortcuts') }}
</h2>
<div class="bg-bg-subtle border border-border rounded-lg p-4 sm:p-6">
<SettingsToggle
:label="$t('settings.keyboard_shortcuts_enabled')"
:description="$t('settings.keyboard_shortcuts_enabled_description')"
v-model="settings.keyboardShortcuts"
/>
</div>
</section>
</div>
</article>
</main>
Expand Down
5 changes: 5 additions & 0 deletions app/utils/prehydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,10 @@ export function initPreferencesOnPrehydrate() {
document.documentElement.dataset.pm = pm

document.documentElement.dataset.collapsed = settings.sidebar?.collapsed?.join(' ') ?? ''

// Keyboard shortcuts (default: true)
if (settings.keyboardShortcuts === false) {
document.documentElement.dataset.kbdShortcuts = 'false'
}
})
}
10 changes: 7 additions & 3 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"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": "You can disable keyboard shortcuts in {settings}."
},
"search": {
"label": "Search npm packages",
Expand Down Expand Up @@ -84,7 +85,8 @@
"appearance": "Appearance",
"display": "Display",
"search": "Data source",
"language": "Language"
"language": "Language",
"keyboard_shortcuts": "Keyboard shortcuts"
},
"data_source": {
"label": "Data source",
Expand All @@ -108,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",
Expand Down
12 changes: 12 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@
},
"open_docs": {
"type": "string"
},
"disable_shortcuts": {
"type": "string"
}
},
"additionalProperties": false
Expand Down Expand Up @@ -258,6 +261,9 @@
},
"language": {
"type": "string"
},
"keyboard_shortcuts": {
"type": "string"
}
},
"additionalProperties": false
Expand Down Expand Up @@ -330,6 +336,12 @@
},
"background_themes": {
"type": "string"
},
"keyboard_shortcuts_enabled": {
"type": "string"
},
"keyboard_shortcuts_enabled_description": {
"type": "string"
}
},
"additionalProperties": false
Expand Down
10 changes: 7 additions & 3 deletions lunaria/files/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"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": "You can disable keyboard shortcuts in {settings}."
},
"search": {
"label": "Search npm packages",
Expand Down Expand Up @@ -83,7 +84,8 @@
"appearance": "Appearance",
"display": "Display",
"search": "Data source",
"language": "Language"
"language": "Language",
"keyboard_shortcuts": "Keyboard shortcuts"
},
"data_source": {
"label": "Data source",
Expand All @@ -107,7 +109,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",
Expand Down
Loading
Loading