added navigation chord shortcuts and shortcut reference dialog demo

This commit is contained in:
Ali Waseem committed 2026-04-16 11:38:44 -06:00
1 parent 79227f5450
commit 803174eaae
5 files changed
+190 -13

No files matched your search

@@ -14,6 +14,7 @@ import { LayoutSidebarProvider } from './ProjectLayout/LayoutSidebar/LayoutSideb
import { ProjectContextProvider } from './ProjectLayout/ProjectContext'
import { AppBannerWrapper } from '@/components/interfaces/App/AppBannerWrapper'
import { Sidebar } from '@/components/interfaces/Sidebar'
import { GlobalShortcuts } from '@/components/ui/GlobalShortcuts/GlobalShortcuts'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useCheckLatestDeploy } from '@/hooks/use-check-latest-deploy'
import { IS_PLATFORM } from '@/lib/constants'
@@ -125,6 +126,7 @@ export const DefaultLayout = ({
<BannerStack />
<StudioMobileSheetNav />
<GlobalShortcuts />
</BannerStackProvider>
</MobileSheetProvider>
</ProjectContextProvider>
@@ -0,0 +1,56 @@
import { useParams } from 'common'
import { useRouter } from 'next/router'
import { useCallback, useState } from 'react'
import { ShortcutsReferenceDialog } from './ShortcutsReferenceDialog'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
/**
* Mounts the app-wide keyboard shortcuts that aren't tied to a specific
* component — navigation chords (G → T/E/S/D/A) and the shortcut reference
* dialog (press ?). Place this inside a layout that stays mounted across
* route changes (e.g. DefaultLayout).
*/
export function GlobalShortcuts() {
const router = useRouter()
const { ref } = useParams()
const [referenceOpen, setReferenceOpen] = useState(false)
const goTo = useCallback(
(path: string) => () => {
if (!ref) return
router.push(`/project/${ref}${path}`)
},
[ref, router]
)
const navEnabled = !!ref
useShortcut(SHORTCUT_IDS.NAV_TABLE_EDITOR, goTo('/editor'), {
enabled: navEnabled,
registerInCommandMenu: true,
})
useShortcut(SHORTCUT_IDS.NAV_SQL_EDITOR, goTo('/sql'), {
enabled: navEnabled,
registerInCommandMenu: true,
})
useShortcut(SHORTCUT_IDS.NAV_STORAGE, goTo('/storage/files'), {
enabled: navEnabled,
registerInCommandMenu: true,
})
useShortcut(SHORTCUT_IDS.NAV_DATABASE, goTo('/database/tables'), {
enabled: navEnabled,
registerInCommandMenu: true,
})
useShortcut(SHORTCUT_IDS.NAV_AUTH, goTo('/auth/users'), {
enabled: navEnabled,
registerInCommandMenu: true,
})
useShortcut(SHORTCUT_IDS.SHORTCUTS_OPEN_REFERENCE, () => setReferenceOpen(true), {
registerInCommandMenu: true,
})
return <ShortcutsReferenceDialog open={referenceOpen} onOpenChange={setReferenceOpen} />
}
@@ -0,0 +1,84 @@
import { Fragment } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogSection,
DialogTitle,
KeyboardShortcut,
} from 'ui'
import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
import { SHORTCUT_DEFINITIONS } from '@/state/shortcuts/registry'
import type { ShortcutDefinition } from '@/state/shortcuts/types'
interface ShortcutsReferenceDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
const GROUP_LABELS: Record<string, string> = {
nav: 'Navigate',
results: 'Results',
shortcuts: 'Help',
}
const groupDefinitions = (): Array<{ group: string; defs: ShortcutDefinition[] }> => {
const grouped = Object.values(SHORTCUT_DEFINITIONS).reduce<Record<string, ShortcutDefinition[]>>(
(acc, def) => {
const prefix = def.id.split('.')[0]
acc[prefix] = acc[prefix] ?? []
acc[prefix].push(def)
return acc
},
{}
)
return Object.entries(grouped).map(([group, defs]) => ({ group, defs }))
}
export function ShortcutsReferenceDialog({ open, onOpenChange }: ShortcutsReferenceDialogProps) {
const groups = groupDefinitions()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent size="medium">
<DialogHeader>
<DialogTitle>Keyboard shortcuts</DialogTitle>
<DialogDescription>
Every shortcut registered in the dashboard. Press <kbd>?</kbd> anytime to reopen this.
</DialogDescription>
</DialogHeader>
<DialogSection className="flex flex-col gap-6 max-h-[60vh] overflow-y-auto">
{groups.map(({ group, defs }) => (
<div key={group} className="flex flex-col gap-2">
<h3 className="text-xs uppercase tracking-wide text-foreground-lighter">
{GROUP_LABELS[group] ?? group}
</h3>
<ul className="flex flex-col">
{defs.map((def) => (
<li
key={def.id}
className="flex items-center justify-between py-2 border-b last:border-b-0"
>
<span className="text-sm text-foreground">{def.label}</span>
<div className="flex items-center gap-1">
{def.sequence.map((step, i) => (
<Fragment key={i}>
{i > 0 && (
<span className="text-foreground-lighter text-[11px]">then</span>
)}
<KeyboardShortcut keys={hotkeyToKeys(step)} />
</Fragment>
))}
</div>
</li>
))}
</ul>
</div>
))}
</DialogSection>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,8 @@
/**
* Convert a TanStack Hotkeys hotkey string (e.g. `"Mod+Shift+M"`) into the
* key-array format consumed by the `<KeyboardShortcut />` component from `ui`
* (e.g. `["Meta", "Shift", "M"]`). `KeyboardShortcut` resolves `Meta` to ⌘ on
* macOS or `Ctrl` elsewhere, so this mapping is platform-safe.
*/
export const hotkeyToKeys = (hotkey: string): string[] =>
hotkey.split('+').map((part) => (part === 'Mod' ? 'Meta' : part))
+40 -13
View File
@@ -9,6 +9,14 @@ import { ShortcutDefinition } from './types'
*/
export const SHORTCUT_IDS = {
RESULTS_COPY_MARKDOWN: 'results.copy-markdown',
NAV_TABLE_EDITOR: 'nav.table-editor',
NAV_SQL_EDITOR: 'nav.sql-editor',
NAV_STORAGE: 'nav.storage',
NAV_DATABASE: 'nav.database',
NAV_AUTH: 'nav.auth',
SHORTCUTS_OPEN_REFERENCE: 'shortcuts.open-reference',
} as const
/**
@@ -23,19 +31,6 @@ export type ShortcutId = (typeof SHORTCUT_IDS)[keyof typeof SHORTCUT_IDS]
* new entry to `SHORTCUT_IDS` without a matching definition here is a type error.
*
* See `ShortcutDefinition` for the shape of each entry.
*
* @example
* // Add a new shortcut:
* // 1. Add to SHORTCUT_IDS:
* // SQL_EDITOR_RUN: 'sql-editor.run'
* // 2. Add to SHORTCUT_DEFINITIONS:
* // [SHORTCUT_IDS.SQL_EDITOR_RUN]: {
* // id: SHORTCUT_IDS.SQL_EDITOR_RUN,
* // label: 'Run query',
* // sequence: ['Mod+Enter'],
* // }
* // 3. Use in a component:
* // useShortcut(SHORTCUT_IDS.SQL_EDITOR_RUN, runQuery)
*/
export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
[SHORTCUT_IDS.RESULTS_COPY_MARKDOWN]: {
@@ -43,4 +38,36 @@ export const SHORTCUT_DEFINITIONS: Record<ShortcutId, ShortcutDefinition> = {
label: 'Copy results as Markdown',
sequence: ['Mod+Shift+M'],
},
[SHORTCUT_IDS.NAV_TABLE_EDITOR]: {
id: SHORTCUT_IDS.NAV_TABLE_EDITOR,
label: 'Go to Table Editor',
sequence: ['G', 'T'],
},
[SHORTCUT_IDS.NAV_SQL_EDITOR]: {
id: SHORTCUT_IDS.NAV_SQL_EDITOR,
label: 'Go to SQL Editor',
sequence: ['G', 'E'],
},
[SHORTCUT_IDS.NAV_STORAGE]: {
id: SHORTCUT_IDS.NAV_STORAGE,
label: 'Go to Storage',
sequence: ['G', 'S'],
},
[SHORTCUT_IDS.NAV_DATABASE]: {
id: SHORTCUT_IDS.NAV_DATABASE,
label: 'Go to Database',
sequence: ['G', 'D'],
},
[SHORTCUT_IDS.NAV_AUTH]: {
id: SHORTCUT_IDS.NAV_AUTH,
label: 'Go to Authentication',
sequence: ['G', 'A'],
},
[SHORTCUT_IDS.SHORTCUTS_OPEN_REFERENCE]: {
id: SHORTCUT_IDS.SHORTCUTS_OPEN_REFERENCE,
label: 'Show all keyboard shortcuts',
sequence: ['Shift+A'],
},
}