Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ venv.bak/
.dmypy.json
dmypy.json

# Local agent metadata
.codex/
.agents/

# Pyre type checker
.pyre/

Expand Down
50 changes: 50 additions & 0 deletions frontend/app/components/keyboard/app-keyboard-shortcuts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { KeyboardShortcutsDialog } from "@components/keyboard/keyboard-shortcuts-dialog"
import { appShortcuts } from "@components/keyboard/shortcut-definitions"
import { useSearchBoxController } from "@components/search/search-box-controller"
import { useSidebar } from "@components/ui/sidebar"
import { useKeyboardShortcuts, useKeyboardShortcutsContext } from "@hooks/use-keyboard-shortcuts"
import { useMemo } from "react"

export const AppKeyboardShortcuts = () => {
const { focusSearch } = useSearchBoxController()
const { openShortcutsDialog } = useKeyboardShortcutsContext()
const { toggleSidebar } = useSidebar()

const shortcuts = useMemo(
() => [
{
description: "Open quick search",
handler: focusSearch,
id: "focus-search",
section: "Global",
sequence: appShortcuts.focusSearch,
},
{
description: "Open quick search with /",
handler: focusSearch,
id: "focus-search-alt",
section: "Global",
sequence: appShortcuts.focusSearchAlt,
},
{
description: "Open keyboard shortcuts",
handler: openShortcutsDialog,
id: "open-help",
section: "Global",
sequence: appShortcuts.openHelp,
},
{
description: "Toggle sidebar",
handler: toggleSidebar,
id: "toggle-sidebar",
section: "Global",
sequence: appShortcuts.toggleSidebar,
},
],
[focusSearch, openShortcutsDialog, toggleSidebar],
)

useKeyboardShortcuts(shortcuts)

return <KeyboardShortcutsDialog />
}
26 changes: 26 additions & 0 deletions frontend/app/components/keyboard/keyboard-shortcuts-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { appShortcuts } from "@components/keyboard/shortcut-definitions"
import { ShortcutHint } from "@components/keyboard/shortcut-hint"
import { Button } from "@components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/ui/tooltip"
import { useKeyboardShortcutsContext } from "@hooks/use-keyboard-shortcuts"
import { Keyboard } from "lucide-react"

export const KeyboardShortcutsButton = () => {
const { openShortcutsDialog } = useKeyboardShortcutsContext()

return (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={openShortcutsDialog} aria-label="Keyboard shortcuts">
<Keyboard className="size-5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="flex items-center gap-2">
<span>Keyboard shortcuts</span>
<ShortcutHint sequence={appShortcuts.openHelp} />
</div>
</TooltipContent>
</Tooltip>
)
}
70 changes: 70 additions & 0 deletions frontend/app/components/keyboard/keyboard-shortcuts-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ShortcutHint } from "@components/keyboard/shortcut-hint"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/ui/dialog"
import { useKeyboardShortcutsContext } from "@hooks/use-keyboard-shortcuts"
import { useMemo } from "react"

export const KeyboardShortcutsDialog = () => {
const { closeShortcutsDialog, shortcuts, shortcutsDialogOpen } = useKeyboardShortcutsContext()

const sections = useMemo(() => {
const grouped = new Map<string, typeof shortcuts>()

for (const shortcut of shortcuts) {
if (shortcut.enabled === false) {
continue
}

const section = shortcut.section ?? "General"
const sectionShortcuts = grouped.get(section) ?? []

if (!sectionShortcuts.some((registeredShortcut) => registeredShortcut.id === shortcut.id)) {
grouped.set(section, [...sectionShortcuts, shortcut])
}
}

return [...grouped.entries()]
}, [shortcuts])

return (
<Dialog open={shortcutsDialogOpen} onOpenChange={(open) => !open && closeShortcutsDialog()}>
<DialogContent className="max-h-[min(36rem,calc(100vh-2rem))] gap-0 overflow-hidden p-0 sm:max-w-2xl">
<DialogHeader className="border-b px-6 py-4">
<DialogTitle>Keyboard shortcuts</DialogTitle>
<DialogDescription>
Available shortcuts update with the page, so route-specific controls appear when relevant.
</DialogDescription>
</DialogHeader>
<div className="overflow-y-auto px-6 py-4">
<div className="space-y-6">
{sections.map(([section, sectionShortcuts]) => (
<section key={section} className="space-y-3">
<h3 className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
{section}
</h3>
<div className="space-y-2">
{sectionShortcuts.map((shortcut) => (
<div
key={`${shortcut.registrationId}:${shortcut.id}`}
className="flex items-center justify-between gap-4 rounded-md border px-3 py-2"
>
<span className="text-sm">{shortcut.description}</span>
<ShortcutHint className="shrink-0" sequence={shortcut.sequence} />
</div>
))}
</div>
</section>
))}
</div>
</div>
<DialogFooter className="border-t px-6 py-4" showCloseButton />
</DialogContent>
</Dialog>
)
}
10 changes: 10 additions & 0 deletions frontend/app/components/keyboard/shortcut-definitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ShortcutTrigger } from "@hooks/use-keyboard-shortcuts"

export const appShortcuts = {
focusSearchAlt: [{ key: "/" }],
openHelp: [{ key: "?", shift: true }],
focusSearch: [{ key: "k", mod: true }],
toggleFacets: [{ key: "f" }],
toggleLiveEventsConnection: [{ key: "c" }],
toggleSidebar: [{ key: "b", mod: true }],
} satisfies Record<string, ShortcutTrigger[]>
75 changes: 75 additions & 0 deletions frontend/app/components/keyboard/shortcut-hint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Kbd } from "@components/ui/kbd"
import { cn } from "@lib/utils"
import type { ShortcutTrigger } from "@hooks/use-keyboard-shortcuts"

const isApplePlatform = () => typeof navigator !== "undefined" && /(mac|iphone|ipad|ipod)/i.test(navigator.userAgent)

const formatKey = (key: string) => {
const normalizedKey = key.toLowerCase()

if (normalizedKey === " ") {
return "Space"
}

if (normalizedKey === "escape") {
return "Esc"
}

if (normalizedKey === "arrowup") {
return "Up"
}

if (normalizedKey === "arrowdown") {
return "Down"
}

if (normalizedKey === "arrowleft") {
return "Left"
}

if (normalizedKey === "arrowright") {
return "Right"
}

return key.length === 1 && /[a-z]/i.test(key) ? key.toUpperCase() : key
}

const getTriggerTokens = (trigger: ShortcutTrigger) => {
const tokens: string[] = []

if (trigger.mod) {
tokens.push(isApplePlatform() ? "Cmd" : "Ctrl")
}

if (trigger.alt) {
tokens.push(isApplePlatform() ? "Opt" : "Alt")
}

if (trigger.shift && /^[a-z0-9]$/i.test(trigger.key)) {
tokens.push("Shift")
}

tokens.push(formatKey(trigger.key))

return tokens
}

interface ShortcutHintProps {
className?: string
sequence: ShortcutTrigger[]
}

export const ShortcutHint = ({ className, sequence }: ShortcutHintProps) => {
return (
<span className={cn("inline-flex items-center gap-1.5 text-muted-foreground", className)}>
{sequence.map((trigger, index) => (
<span key={`${trigger.key}-${index}`} className="inline-flex items-center gap-1">
{getTriggerTokens(trigger).map((token) => (
<Kbd key={token}>{token}</Kbd>
))}
{index < sequence.length - 1 ? <span className="text-xs">then</span> : null}
</span>
))}
</span>
)
}
9 changes: 8 additions & 1 deletion frontend/app/components/raw_events/toggle-connect.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { appShortcuts } from "@components/keyboard/shortcut-definitions"
import { ShortcutHint } from "@components/keyboard/shortcut-hint"
import { Button } from "@components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/ui/tooltip"
import { Pause, Play } from "lucide-react"
Expand All @@ -17,7 +19,12 @@ export const ToggleConnect: React.FC<ToggleConnectProps> = ({ connect, setConnec
{connect ? <Pause className="size-4" /> : <Play className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>{connect ? "Freeze" : "Connect"}</TooltipContent>
<TooltipContent>
<div className="flex items-center gap-2">
<span>{connect ? "Freeze" : "Connect"}</span>
{!disabled ? <ShortcutHint sequence={appShortcuts.toggleLiveEventsConnection} /> : null}
</div>
</TooltipContent>
</Tooltip>
)
}
Loading
Loading