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
3 changes: 3 additions & 0 deletions packages/layer/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ export default defineAppConfig({
url: '',
branch: 'main',
},
shortcuts: {
toggleColorMode: 'd',
},
},
})
73 changes: 73 additions & 0 deletions packages/layer/app/composables/useDocsShortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { onKeyStroke } from '@vueuse/core'
Comment thread
amondnet marked this conversation as resolved.
import { computed } from 'vue'

/**
* Returns `true` when the keystroke originated from an editable element
* (`<input>`, `<textarea>`, or any element with `[contenteditable]`),
* in which case the shortcut should be ignored so the user can type freely.
*
* Traverses up the DOM tree from the event target so that non-`HTMLElement`
* descendants (e.g. `SVGElement`, `MathMLElement`) inside an editable
* container are still treated as editable.
*/
function isEditableTarget(target: EventTarget | null): boolean {
let el: Node | null = target instanceof Node ? target : null
while (el && !(el instanceof HTMLElement)) {
el = el.parentNode
}

if (!el)
return false

const tag = el.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
return true

if (el.isContentEditable)
return true

return false
}
Comment thread
amondnet marked this conversation as resolved.

/**
* Registers a keyboard shortcut (default `d`) that toggles between
* light and dark color modes. Mirrors docus' `useDocusShortcuts`
* composable (upstream commit `61c36d03`) but is reimplemented on top of
* `@vueuse/core`'s `onKeyStroke` instead of `@nuxt/ui`'s `defineShortcuts`.
*
* The handler bails out when:
* - the color mode is forced (e.g. `colorMode.forced === true`),
* - the keystroke originated from an editable element,
* - any modifier key (meta / ctrl / alt / shift) is pressed,
* - the configured shortcut is empty.
*/
export function useDocsShortcuts(): void {
const appConfig = useAppConfig()
const colorMode = useColorMode()

const toggleColorModeShortcut = computed<string>(
() => appConfig.docs?.shortcuts?.toggleColorMode ?? 'd',
)

onKeyStroke(
(event: KeyboardEvent) => {
const key = toggleColorModeShortcut.value
if (!key)
return false
return event.key?.toLowerCase() === key.toLowerCase()
},
(event: KeyboardEvent) => {
if (colorMode.forced)
return

if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
return

if (isEditableTarget(event.target))
return

const resolved = colorMode.value === 'dark' ? 'light' : 'dark'
colorMode.preference = resolved
},
)
}
9 changes: 9 additions & 0 deletions packages/layer/app/plugins/shortcuts.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Installs the documentation layer's keyboard shortcuts on app boot.
*
* Client-only because the shortcut targets `window` keystrokes, which
* have no meaning on the server.
*/
export default defineNuxtPlugin(() => {
useDocsShortcuts()
})
3 changes: 3 additions & 0 deletions packages/layer/modules/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export default defineNuxtModule({
url: gitInfo?.url,
branch: getGitBranch(),
},
shortcuts: {
toggleColorMode: 'd',
},
})

// SEO defaults
Expand Down
14 changes: 14 additions & 0 deletions packages/layer/nuxt.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ export default defineNuxtSchema({
},
},
},
shortcuts: {
$schema: {
title: 'Keyboard Shortcuts',
description: 'Keyboard shortcuts configuration',
},
toggleColorMode: {
$default: 'd',
$schema: {
title: 'Toggle Color Mode',
description: 'Shortcut to toggle light and dark mode. Leave empty to disable.',
type: 'string',
},
},
},
},
},
})