feat: add keyboard shortcut hints to toolbar #774
feat: add keyboard shortcut hints to toolbar #774Junaid-Kn wants to merge 4 commits intoOpenCut-app:mainfrom
Conversation
|
@Junaid-Kn is attempting to deploy a commit to the OpenCut OSS Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR adds visual keyboard shortcut hints to toolbar buttons by introducing a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx (1)
99-104: MemoizeshortcutMapand prefer typed lookup.
shortcutMapis rebuilt withObject.fromEntrieson every render, and typing is lost (Record<string, KeyboardShortcut>at best). ConsideruseMemokeyed onshortcuts, and use aMap<TActionWithOptionalArgs, KeyboardShortcut>so lookups are action-typed. Also the trailing tab + blank lines on 102-104 appear to be stray whitespace.- const { shortcuts } = useKeyboardShortcutsHelp(); - const shortcutMap = Object.fromEntries( - shortcuts.map(s => [s.action, s]) - ); - - + const { shortcuts } = useKeyboardShortcutsHelp(); + const shortcutMap = useMemo( + () => new Map(shortcuts.map((s) => [s.action, s] as const)), + [shortcuts], + );Then callers become
shortcutMap.get("split")?.keys?.[0], etc.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx` around lines 99 - 104, The current code recreates shortcutMap on every render and loses typing; wrap the conversion in useMemo keyed on shortcuts and change the structure from a plain object to a typed Map so lookups are action-typed (e.g. Map<TActionWithOptionalArgs, KeyboardShortcut>); update all callers to use shortcutMap.get("split")?.keys?.[0] (or the appropriate action literal) and remove the stray trailing tab/blank lines after the shortcutMap declaration in timeline-toolbar.tsx; keep the source of shortcuts via useKeyboardShortcutsHelp() unchanged.apps/web/src/components/ui/kbd.tsx (1)
1-19: Minor: unnecessary"use client"and stray whitespace.
Kbdis a purely presentational component with no hooks or interactivity, so the"use client"directive isn't required and pulls it into the client bundle needlessly. Alsoexport const Kbd = ((line 8) has a double space, and the empty line inside the destructure (line 12) looks unintentional.♻️ Proposed tidy-up
-"use client" import * as React from "react"; import { cn } from "@/utils/ui"; type KbdProps = { - children: React.ReactNode; - className?: string; -} -export const Kbd = ( - { - children, - className - - }: KbdProps) => { + children: React.ReactNode; + className?: string; +}; +export const Kbd = ({ children, className }: KbdProps) => { return ( <kbd className={cn("rounded bg-muted px-1.5 py-0.5 text-xs font-mono border", className)}> {children} </kbd> ); -} +};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/ui/kbd.tsx` around lines 1 - 19, Remove the unnecessary "use client" directive and tidy up whitespace in the Kbd component: delete the top-line "use client", collapse the extra space in the export declaration for export const Kbd, and remove the empty line inside the destructured props. Locate the Kbd component and its type KbdProps (and the cn import) to make these edits so the presentational component remains server-safe and formatting is consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/components/editor/panels/preview/toolbar.tsx`:
- Around line 144-166: Replace the incorrect prop name on TooltipProvider:
change the prop currently named duration to delayDuration and set it to 500
(i.e., TooltipProvider delayDuration={500}) so Radix's hover delay is applied;
additionally, prevent rendering an empty keyboard badge by conditionally
rendering the <Kbd> element only when togglePlay?.keys exists and has length
(e.g., check togglePlay?.keys?.length > 0) inside the TooltipContent where
togglePlay?.description and keys are shown—this touches the TooltipProvider,
TooltipContent and Kbd usages around invokeAction("toggle-play") and the
togglePlay references.
In `@apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx`:
- Around line 255-262: The list rendered from Object.values(ToolTipShortcuts) is
missing a stable React key; update the mapping to iterate with
Object.entries(ToolTipShortcuts) (or retain values but obtain the unique action
key) and pass a key prop to each ToolbarButton using the unique action
identifier (e.g., the ToolTipShortcuts entry key or action name) so that
ToolbarButton receives key={uniqueKey} along with the existing props (icon,
disabled, tooltip, onClick).
In `@package.json`:
- Around line 30-32: The root package.json currently declares "opencut-wasm":
"^0.2.5" which conflicts with the apps/web/package.json version "^0.2.6" and
will shadow the app's version in a Bun monorepo; remove the "opencut-wasm" entry
from the root package.json so the app-level dependency in apps/web/package.json
(used by files like toolbar.tsx and various timeline managers/hooks) is the
single source of truth, or if the root truly needs it, update both package.json
files to the same version (^0.2.6) and add a short comment in the root
explaining why root requires the dependency; after changing package.json, run
your install to update the lockfile.
---
Nitpick comments:
In `@apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx`:
- Around line 99-104: The current code recreates shortcutMap on every render and
loses typing; wrap the conversion in useMemo keyed on shortcuts and change the
structure from a plain object to a typed Map so lookups are action-typed (e.g.
Map<TActionWithOptionalArgs, KeyboardShortcut>); update all callers to use
shortcutMap.get("split")?.keys?.[0] (or the appropriate action literal) and
remove the stray trailing tab/blank lines after the shortcutMap declaration in
timeline-toolbar.tsx; keep the source of shortcuts via
useKeyboardShortcutsHelp() unchanged.
In `@apps/web/src/components/ui/kbd.tsx`:
- Around line 1-19: Remove the unnecessary "use client" directive and tidy up
whitespace in the Kbd component: delete the top-line "use client", collapse the
extra space in the export declaration for export const Kbd, and remove the empty
line inside the destructured props. Locate the Kbd component and its type
KbdProps (and the cn import) to make these edits so the presentational component
remains server-safe and formatting is consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4fe5a024-aa6f-413d-90c9-790ccba5ac5d
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
apps/web/src/components/editor/panels/preview/toolbar.tsxapps/web/src/components/editor/panels/timeline/timeline-toolbar.tsxapps/web/src/components/ui/kbd.tsxpackage.json
| return ( | ||
| <Button | ||
| variant="text" | ||
| size="icon" | ||
| onClick={() => invokeAction("toggle-play")} | ||
| > | ||
| <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} /> | ||
| </Button> | ||
| <TooltipProvider duration={500}> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button | ||
| variant="text" | ||
| size="icon" | ||
| onClick={() => invokeAction("toggle-play")} | ||
| > | ||
| <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} /> | ||
| </Button> | ||
| </TooltipTrigger> | ||
|
|
||
| <TooltipContent className="flex items-center gap-2"> | ||
| <span> {togglePlay?.description} </span> | ||
| <div className="flex gap-1"> | ||
| <Kbd> | ||
| {togglePlay?.keys?.join(" / ")} | ||
| </Kbd> | ||
| </div> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| </TooltipProvider> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the correct prop name for configuring the hover delay on Radix UI TooltipProvider?
💡 Result:
The correct prop name for configuring the hover delay on Radix UI TooltipProvider is delayDuration. It controls the duration (in milliseconds) from when the mouse enters a tooltip trigger until the tooltip opens, with a default value of 700ms. This is confirmed in the official Radix Primitives documentation, which lists delayDuration under TooltipProvider props. Examples show usage like <Tooltip.Provider delayDuration={200}>. The same prop is available on Tooltip.Root for per-tooltip overrides, and skipDelayDuration (default 300ms) handles delays when moving between triggers.
Citations:
- 1: https://www.radix-ui.com/docs/primitives/components/tooltip
- 2: https://www.radix-ui.com/primitives/docs/components/tooltip
- 3: https://radix-ui-primitives.mintlify.app/components/tooltip
🏁 Script executed:
# First, locate the toolbar.tsx file
find . -name "toolbar.tsx" -path "*/preview/*" 2>/dev/nullRepository: OpenCut-app/OpenCut
Length of output: 122
🏁 Script executed:
# Read the specific lines from toolbar.tsx to confirm the code
if [ -f "apps/web/src/components/editor/panels/preview/toolbar.tsx" ]; then
sed -n '144,166p' "apps/web/src/components/editor/panels/preview/toolbar.tsx" | cat -n
fiRepository: OpenCut-app/OpenCut
Length of output: 831
🏁 Script executed:
# Check timeline-toolbar.tsx for the delayDuration usage references
if [ -f "apps/web/src/components/editor/panels/timeline-toolbar.tsx" ]; then
sed -n '250,260p' "apps/web/src/components/editor/panels/timeline-toolbar.tsx" | cat -n
echo "---"
sed -n '345,355p' "apps/web/src/components/editor/panels/timeline-toolbar.tsx" | cat -n
fiRepository: OpenCut-app/OpenCut
Length of output: 45
🏁 Script executed:
# Search for togglePlay definition in toolbar.tsx to understand its type
if [ -f "apps/web/src/components/editor/panels/preview/toolbar.tsx" ]; then
head -150 "apps/web/src/components/editor/panels/preview/toolbar.tsx" | grep -n "togglePlay" | head -10
fiRepository: OpenCut-app/OpenCut
Length of output: 102
🏁 Script executed:
# Get more context around togglePlay definition
if [ -f "apps/web/src/components/editor/panels/preview/toolbar.tsx" ]; then
sed -n '135,150p' "apps/web/src/components/editor/panels/preview/toolbar.tsx" | cat -n
fiRepository: OpenCut-app/OpenCut
Length of output: 565
🏁 Script executed:
# Search for timeline-toolbar.tsx in the editor panels directory
find . -name "timeline-toolbar.tsx" 2>/dev/null | head -5Repository: OpenCut-app/OpenCut
Length of output: 132
🏁 Script executed:
# Check the imports at the top of toolbar.tsx to verify TooltipProvider source
if [ -f "apps/web/src/components/editor/panels/preview/toolbar.tsx" ]; then
head -30 "apps/web/src/components/editor/panels/preview/toolbar.tsx" | cat -n
fiRepository: OpenCut-app/OpenCut
Length of output: 1356
🏁 Script executed:
# Check the actual usage in timeline-toolbar.tsx at lines 254 and 348
if [ -f "apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx" ]; then
sed -n '250,260p' "apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx" | cat -n
echo "---"
sed -n '344,354p' "apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx" | cat -n
fiRepository: OpenCut-app/OpenCut
Length of output: 850
🏁 Script executed:
# Search for all TooltipProvider usages in the codebase to see the pattern
rg "TooltipProvider" -A 2 -B 1 apps/web/src/components/Repository: OpenCut-app/OpenCut
Length of output: 6671
Change duration to delayDuration on TooltipProvider.
Radix UI's TooltipProvider accepts delayDuration, not duration. The prop is silently ignored, so the intended 500ms hover delay has no effect. This file is the only one using the incorrect prop name; all other usages in the codebase (timeline-toolbar.tsx, properties/index.tsx) correctly use delayDuration={500}.
Also, guard the <Kbd> element with a conditional to avoid rendering empty badges when togglePlay?.keys is undefined or empty:
Proposed fix
- <TooltipProvider duration={500}>
+ <TooltipProvider delayDuration={500}>
<Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="text"
- size="icon"
- onClick={() => invokeAction("toggle-play")}
- >
- <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
- </Button>
- </TooltipTrigger>
-
- <TooltipContent className="flex items-center gap-2">
- <span> {togglePlay?.description} </span>
- <div className="flex gap-1">
- <Kbd>
- {togglePlay?.keys?.join(" / ")}
- </Kbd>
- </div>
- </TooltipContent>
- </Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="text"
+ size="icon"
+ onClick={() => invokeAction("toggle-play")}
+ >
+ <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent className="flex items-center gap-2">
+ <span>{togglePlay?.description}</span>
+ {togglePlay?.keys?.length ? (
+ <Kbd>{togglePlay.keys.join(" / ")}</Kbd>
+ ) : null}
+ </TooltipContent>
+ </Tooltip>
</TooltipProvider>📝 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.
| return ( | |
| <Button | |
| variant="text" | |
| size="icon" | |
| onClick={() => invokeAction("toggle-play")} | |
| > | |
| <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} /> | |
| </Button> | |
| <TooltipProvider duration={500}> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| variant="text" | |
| size="icon" | |
| onClick={() => invokeAction("toggle-play")} | |
| > | |
| <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent className="flex items-center gap-2"> | |
| <span> {togglePlay?.description} </span> | |
| <div className="flex gap-1"> | |
| <Kbd> | |
| {togglePlay?.keys?.join(" / ")} | |
| </Kbd> | |
| </div> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| return ( | |
| <TooltipProvider delayDuration={500}> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| variant="text" | |
| size="icon" | |
| onClick={() => invokeAction("toggle-play")} | |
| > | |
| <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent className="flex items-center gap-2"> | |
| <span>{togglePlay?.description}</span> | |
| {togglePlay?.keys?.length ? ( | |
| <Kbd>{togglePlay.keys.join(" / ")}</Kbd> | |
| ) : null} | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/editor/panels/preview/toolbar.tsx` around lines 144 -
166, Replace the incorrect prop name on TooltipProvider: change the prop
currently named duration to delayDuration and set it to 500 (i.e.,
TooltipProvider delayDuration={500}) so Radix's hover delay is applied;
additionally, prevent rendering an empty keyboard badge by conditionally
rendering the <Kbd> element only when togglePlay?.keys exists and has length
(e.g., check togglePlay?.keys?.length > 0) inside the TooltipContent where
togglePlay?.description and keys are shown—this touches the TooltipProvider,
TooltipContent and Kbd usages around invokeAction("toggle-play") and the
togglePlay references.
| {Object.values(ToolTipShortcuts).map((item)=> ( | ||
| <ToolbarButton | ||
| icon={item.icon} | ||
| disabled={item.disabled} | ||
| tooltip={item.tooltip} | ||
| onClick={item.onClick} | ||
| /> | ||
| } | ||
| tooltip={sourceAudioLabel} | ||
| disabled={!canToggleSelectedSourceAudio} | ||
| onClick={({ event }) => | ||
| handleAction({ action: "toggle-source-audio", event }) | ||
| } | ||
| /> | ||
|
|
||
| <ToolbarButton | ||
| icon={<HugeiconsIcon icon={Copy01Icon} />} | ||
| tooltip="Duplicate element" | ||
| onClick={({ event }) => | ||
| handleAction({ action: "duplicate-selected", event }) | ||
| } | ||
| /> | ||
|
|
||
| <ToolbarButton | ||
| icon={<HugeiconsIcon icon={SnowIcon} />} | ||
| tooltip="Freeze frame (coming soon)" | ||
| disabled={true} | ||
| onClick={({ event: _event }) => {}} | ||
| /> | ||
|
|
||
| <ToolbarButton | ||
| icon={<HugeiconsIcon icon={Delete02Icon} />} | ||
| tooltip="Delete element" | ||
| onClick={({ event }) => | ||
| handleAction({ action: "delete-selected", event }) | ||
| } | ||
| /> | ||
| ))} |
There was a problem hiding this comment.
Missing key prop in iterable.
Biome flags this correctly: React requires a stable key when rendering a list from .map(). Since each entry in ToolTipShortcuts has a unique action key, iterate with Object.entries and use the key.
🔑 Proposed fix
- {Object.values(ToolTipShortcuts).map((item)=> (
- <ToolbarButton
- icon={item.icon}
- disabled={item.disabled}
- tooltip={item.tooltip}
- onClick={item.onClick}
- />
- ))}
+ {Object.entries(ToolTipShortcuts).map(([key, item]) => (
+ <ToolbarButton
+ key={key}
+ icon={item.icon}
+ disabled={item.disabled}
+ tooltip={item.tooltip}
+ onClick={item.onClick}
+ />
+ ))}📝 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.
| {Object.values(ToolTipShortcuts).map((item)=> ( | |
| <ToolbarButton | |
| icon={item.icon} | |
| disabled={item.disabled} | |
| tooltip={item.tooltip} | |
| onClick={item.onClick} | |
| /> | |
| } | |
| tooltip={sourceAudioLabel} | |
| disabled={!canToggleSelectedSourceAudio} | |
| onClick={({ event }) => | |
| handleAction({ action: "toggle-source-audio", event }) | |
| } | |
| /> | |
| <ToolbarButton | |
| icon={<HugeiconsIcon icon={Copy01Icon} />} | |
| tooltip="Duplicate element" | |
| onClick={({ event }) => | |
| handleAction({ action: "duplicate-selected", event }) | |
| } | |
| /> | |
| <ToolbarButton | |
| icon={<HugeiconsIcon icon={SnowIcon} />} | |
| tooltip="Freeze frame (coming soon)" | |
| disabled={true} | |
| onClick={({ event: _event }) => {}} | |
| /> | |
| <ToolbarButton | |
| icon={<HugeiconsIcon icon={Delete02Icon} />} | |
| tooltip="Delete element" | |
| onClick={({ event }) => | |
| handleAction({ action: "delete-selected", event }) | |
| } | |
| /> | |
| ))} | |
| {Object.entries(ToolTipShortcuts).map(([key, item]) => ( | |
| <ToolbarButton | |
| key={key} | |
| icon={item.icon} | |
| disabled={item.disabled} | |
| tooltip={item.tooltip} | |
| onClick={item.onClick} | |
| /> | |
| ))} |
🧰 Tools
🪛 Biome (2.4.11)
[error] 256-261: Missing key property for this element in iterable.
(lint/correctness/useJsxKeyInIterable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx` around
lines 255 - 262, The list rendered from Object.values(ToolTipShortcuts) is
missing a stable React key; update the mapping to iterate with
Object.entries(ToolTipShortcuts) (or retain values but obtain the unique action
key) and pass a key prop to each ToolbarButton using the unique action
identifier (e.g., the ToolTipShortcuts entry key or action name) so that
ToolbarButton receives key={uniqueKey} along with the existing props (icon,
disabled, tooltip, onClick).
| "next": "^16.1.3", | ||
| "opencut-wasm": "^0.2.5" | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm where opencut-wasm is actually consumed and whether apps/web already declares it.
rg -nP "from ['\"]opencut-wasm['\"]" --type=ts --type=tsx
fd -t f package.json | xargs -I{} sh -c 'echo "=== {} ==="; jq ".dependencies[\"opencut-wasm\"] // empty" {}'Repository: OpenCut-app/OpenCut
Length of output: 176
🏁 Script executed:
# Search for any import/require of opencut-wasm with broader patterns
rg -i "opencut-wasm" --type=js --type=ts
# Also check if the specific file exists
find . -path "*preview/toolbar*" -type f
# Check the workspace configuration
fd -t f "package.json|yarn.lock|bun.lock|pnpm-workspace.yaml" | head -5Repository: OpenCut-app/OpenCut
Length of output: 3505
🏁 Script executed:
# Check bun workspace configuration
cat package.json | jq '.workspaces'
# Check if root has direct dependencies that would need opencut-wasm
rg "opencut-wasm" package.json apps/package.json 2>/dev/null | head -20
# Look at bun.lock to see how versions are resolved
head -100 bun.lock | grep -A5 -B5 "opencut-wasm" || echo "Pattern not in first 100 lines; searching entire lock file..."
rg "opencut-wasm" bun.lock | head -20Repository: OpenCut-app/OpenCut
Length of output: 1004
Version conflict — opencut-wasm appears in both root and app package.json with different versions.
This PR is scoped to keyboard-shortcut tooltip hints, but it adds opencut-wasm to the root package.json with version ^0.2.5. However, opencut-wasm is already declared in apps/web/package.json with version ^0.2.6, and all 30+ imports are confined to apps/web (e.g., formatTimecode in toolbar.tsx, timeline managers, hooks, etc.). In a Bun monorepo, the root version will shadow the higher app-level version, causing the lower version (0.2.5) to resolve instead of 0.2.6.
Remove this dependency from the root package.json, or if there's a legitimate reason for it there, align both declarations to the same version and document why root needs it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` around lines 30 - 32, The root package.json currently declares
"opencut-wasm": "^0.2.5" which conflicts with the apps/web/package.json version
"^0.2.6" and will shadow the app's version in a Bun monorepo; remove the
"opencut-wasm" entry from the root package.json so the app-level dependency in
apps/web/package.json (used by files like toolbar.tsx and various timeline
managers/hooks) is the single source of truth, or if the root truly needs it,
update both package.json files to the same version (^0.2.6) and add a short
comment in the root explaining why root requires the dependency; after changing
package.json, run your install to update the lockfile.
Summary
This PR implements #732.
It adds keyboard shortcut indicators to tooltips across the relevant UI components using
<kbd />tags for better accessibility and discoverability.Changes Made
<kbd />componentsWhy this change?
This improves usability by making keyboard shortcuts more visible and easier to learn, especially for power users.
Related Issue
Closes #732
Notes
No breaking changes. UI-only improvement.
Summary by CodeRabbit