ShortcutsReferenceSheet.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. // @ts-nocheck
  2. import { useHotkeyRegistrations, type SequenceRegistrationView } from '@tanstack/react-hotkeys'
  3. import { CircleX } from 'lucide-react'
  4. import { Fragment, useMemo, useState } from 'react'
  5. import {
  6. Button,
  7. KeyboardShortcut,
  8. Sheet,
  9. SheetContent,
  10. SheetDescription,
  11. SheetHeader,
  12. SheetSection,
  13. SheetTitle,
  14. } from 'ui'
  15. import { Input } from 'ui-patterns/DataInputs/Input'
  16. import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
  17. import {
  18. SHORTCUT_REFERENCE_GROUP_LABELS,
  19. SHORTCUT_REFERENCE_GROUP_ORDER,
  20. SHORTCUT_REFERENCE_GROUPS,
  21. } from '@/state/shortcuts/referenceGroups'
  22. import type { ShortcutHotkeyMeta } from '@/state/shortcuts/useShortcut'
  23. interface ShortcutsReferenceSheetProps {
  24. open: boolean
  25. onOpenChange: (open: boolean) => void
  26. }
  27. interface ActiveShortcutDefinition {
  28. id: string
  29. label: string
  30. sequence: string[]
  31. referenceGroup?: string
  32. }
  33. interface ShortcutGroup {
  34. group: string
  35. label: string
  36. definitions: ActiveShortcutDefinition[]
  37. }
  38. const GROUP_LABELS: Record<string, string> = {
  39. ...SHORTCUT_REFERENCE_GROUP_LABELS,
  40. 'action-bar': 'Actions',
  41. 'ai-assistant': 'AI Assistant',
  42. 'auth-users': 'Auth Users',
  43. 'command-menu': 'Command Menu',
  44. 'data-table': 'Data Tables',
  45. 'functions-detail': 'Edge Function Actions',
  46. 'functions-list': 'Edge Functions',
  47. 'functions-overview': 'Edge Function Overview',
  48. 'inline-editor': 'Inline Editor',
  49. 'list-page': 'List pages',
  50. 'logs-preview': 'Logs Explorer',
  51. nav: 'Navigation',
  52. 'operation-queue': 'Operation Queue',
  53. results: 'Results',
  54. 'realtime-inspector': 'Realtime Inspector',
  55. 'schema-visualizer': 'Schema Visualizer',
  56. shortcuts: 'Shortcuts',
  57. 'sql-editor': 'SQL Editor',
  58. 'storage-buckets': 'Storage Buckets',
  59. 'storage-explorer': 'Storage File Explorer',
  60. 'table-editor': 'Table Editor',
  61. 'unified-logs': 'Logs',
  62. }
  63. const getGroupOrder = (group: string) => {
  64. const index = SHORTCUT_REFERENCE_GROUP_ORDER.indexOf(group)
  65. return index === -1 ? SHORTCUT_REFERENCE_GROUP_ORDER.length : index
  66. }
  67. const getGroupLabel = (group: string) => GROUP_LABELS[group] ?? group
  68. const isScopedNavigationGroup = (group: string) =>
  69. group.startsWith('navigation.') && group !== SHORTCUT_REFERENCE_GROUPS.NAVIGATION_GLOBAL
  70. const normalizeSearchValue = (value: string) => value.trim().toLowerCase()
  71. const toActiveDefinition = (
  72. registration: SequenceRegistrationView
  73. ): ActiveShortcutDefinition | null => {
  74. const meta = registration.options.meta as ShortcutHotkeyMeta | undefined
  75. if (!meta?.id || !meta.name) return null
  76. return {
  77. id: meta.id,
  78. label: meta.name,
  79. sequence: registration.sequence,
  80. referenceGroup: meta.referenceGroup,
  81. }
  82. }
  83. const useActiveShortcuts = (): ActiveShortcutDefinition[] => {
  84. const { sequences } = useHotkeyRegistrations()
  85. return useMemo(() => {
  86. const definitions: ActiveShortcutDefinition[] = []
  87. const seen = new Set<string>()
  88. for (const registration of sequences) {
  89. if (registration.options.enabled === false) continue
  90. const definition = toActiveDefinition(registration)
  91. if (!definition) continue
  92. if (seen.has(definition.id)) continue
  93. seen.add(definition.id)
  94. definitions.push(definition)
  95. }
  96. return definitions
  97. }, [sequences])
  98. }
  99. const groupDefinitions = (activeShortcuts: ActiveShortcutDefinition[]): ShortcutGroup[] => {
  100. const grouped = activeShortcuts.reduce<Record<string, ActiveShortcutDefinition[]>>(
  101. (acc, definition) => {
  102. const prefix = definition.referenceGroup ?? definition.id.split('.')[0]
  103. acc[prefix] = acc[prefix] ?? []
  104. acc[prefix].push(definition)
  105. return acc
  106. },
  107. {}
  108. )
  109. const hasScopedNavigationGroup = Object.keys(grouped).some(isScopedNavigationGroup)
  110. return Object.entries(grouped)
  111. .map(([group, definitions]) => {
  112. const label =
  113. group === SHORTCUT_REFERENCE_GROUPS.NAVIGATION_GLOBAL && !hasScopedNavigationGroup
  114. ? 'Navigation'
  115. : getGroupLabel(group)
  116. return {
  117. group,
  118. label,
  119. definitions,
  120. }
  121. })
  122. .sort((a, b) => getGroupOrder(a.group) - getGroupOrder(b.group))
  123. }
  124. const filterGroups = (groups: ShortcutGroup[], search: string) => {
  125. const normalizedSearch = normalizeSearchValue(search)
  126. if (normalizedSearch.length === 0) return groups
  127. return groups.reduce<ShortcutGroup[]>((acc, group) => {
  128. if (normalizeSearchValue(group.label).includes(normalizedSearch)) {
  129. acc.push(group)
  130. return acc
  131. }
  132. const definitions = group.definitions.filter((definition) =>
  133. normalizeSearchValue(definition.label).includes(normalizedSearch)
  134. )
  135. if (definitions.length > 0) {
  136. acc.push({ ...group, definitions })
  137. }
  138. return acc
  139. }, [])
  140. }
  141. const ShortcutSequence = ({ sequence }: Pick<ActiveShortcutDefinition, 'sequence'>) => (
  142. <div className="flex items-center gap-1">
  143. {sequence.map((step, index) => (
  144. <Fragment key={`${step}-${index}`}>
  145. {index > 0 && <span className="text-foreground-lighter text-[11px]">then</span>}
  146. <KeyboardShortcut keys={hotkeyToKeys(step)} variant="pill" />
  147. </Fragment>
  148. ))}
  149. </div>
  150. )
  151. function ShortcutsReferenceSheetContent() {
  152. const [search, setSearch] = useState('')
  153. const activeShortcuts = useActiveShortcuts()
  154. const groups = filterGroups(groupDefinitions(activeShortcuts), search)
  155. return (
  156. <>
  157. <SheetHeader className="shrink-0 py-3">
  158. <SheetTitle>Keyboard shortcuts</SheetTitle>
  159. <SheetDescription className="sr-only">
  160. Browse and search available keyboard shortcuts.
  161. </SheetDescription>
  162. </SheetHeader>
  163. <div className="shrink-0 bg-studio px-5 pt-4 pb-4">
  164. <Input
  165. aria-label="Search shortcuts"
  166. autoFocus
  167. className="w-full"
  168. onChange={(event) => setSearch(event.target.value)}
  169. placeholder="Search shortcuts..."
  170. value={search}
  171. actions={
  172. search ? (
  173. <Button
  174. aria-label="Clear search"
  175. size="tiny"
  176. type="text"
  177. icon={<CircleX size={14} />}
  178. onClick={() => setSearch('')}
  179. className="h-5 w-5 p-0"
  180. />
  181. ) : null
  182. }
  183. />
  184. </div>
  185. <SheetSection className="flex flex-1 flex-col gap-6 overflow-y-auto px-5 py-4">
  186. {groups.length === 0 ? (
  187. <p className="text-sm text-foreground-muted">No matching shortcuts found</p>
  188. ) : (
  189. groups.map(({ group, label, definitions }) => (
  190. <section key={group} className="flex flex-col gap-2">
  191. <h3 className="text-xs text-foreground-lighter uppercase tracking-wider">{label}</h3>
  192. <ul className="flex flex-col">
  193. {definitions.map((definition) => (
  194. <li
  195. key={definition.id}
  196. className="flex min-h-10 items-center justify-between gap-4 border-b border-muted py-2 last:border-b-0"
  197. >
  198. <span className="text-sm text-foreground">{definition.label}</span>
  199. <ShortcutSequence sequence={definition.sequence} />
  200. </li>
  201. ))}
  202. </ul>
  203. </section>
  204. ))
  205. )}
  206. </SheetSection>
  207. </>
  208. )
  209. }
  210. export function ShortcutsReferenceSheet({ open, onOpenChange }: ShortcutsReferenceSheetProps) {
  211. return (
  212. <Sheet open={open} onOpenChange={onOpenChange}>
  213. <SheetContent className="flex w-full flex-col gap-0 p-0 sm:max-w-[520px]">
  214. {open && <ShortcutsReferenceSheetContent />}
  215. </SheetContent>
  216. </Sheet>
  217. )
  218. }