feat: allow clicking keyboard hints throughout (#266)

* allow clicking keyboard hints

* Update js/desktop/src/components/key-hint.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* nits

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit was merged in pull request #266.
This commit is contained in:
Arjun Patel
2026-06-11 10:48:59 -07:00
committed by GitHub
co-authored by coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent 9fd7e611f3
commit 95f2362947
15 changed files with 283 additions and 208 deletions
@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Button } from '@/components/ui/button';
import { KeyHint } from '@/components/key-hint';
interface ConfirmDestructiveOverlayProps {
title: string;
@@ -43,12 +44,14 @@ export function ConfirmDestructiveOverlay({
<div className="absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur-xl">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold text-white/70">{title}</h2>
<span className="text-xs text-white/30">
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Esc
</kbd>{' '}
<KeyHint
keys="Esc"
onClick={onClose}
title="Close (or press Esc)"
className="text-xs text-white/30"
>
to close
</span>
</KeyHint>
</div>
<div className="text-sm text-white/60">{description}</div>
+79
View File
@@ -0,0 +1,79 @@
import { Fragment } from 'react';
import { Kbd } from '@/components/ui/kbd';
import { cn } from '@/lib/utils';
/** Dark-overlay chip restyle of the design-system Kbd, kept in one place. */
const chipClass =
'rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs font-normal text-current';
interface KeyHintProps {
/** Key chip(s): "Esc" or ["Esc", "Q"]. */
keys: string | string[];
/** Rendered between chips, e.g. "or". Defaults to a plain space. */
separator?: React.ReactNode;
/** Text before the first chip, e.g. "Release". */
prefix?: React.ReactNode;
/** Trailing label, e.g. "cancel". May contain icons. */
children?: React.ReactNode;
/** When set, renders a <button> with hover affordance; otherwise a plain <span>. */
onClick?: () => void;
/** Tooltip explaining the action; pass alongside onClick. */
title?: string;
className?: string;
'aria-label'?: string;
}
/**
* A keyboard-shortcut hint: one or more key chips with optional surrounding
* text. Keyboard-first, but every hint with an `onClick` is also a real
* button so mouse users can trigger the same action by clicking it.
*/
export function KeyHint({
keys,
separator,
prefix,
children,
onClick,
title,
className,
...rest
}: KeyHintProps) {
const keyList = Array.isArray(keys) ? keys : [keys];
const content = (
<>
{prefix != null && <>{prefix} </>}
{keyList.map((k, i) => (
<Fragment key={`${k}-${i}`}>
{i > 0 && (separator != null ? <> {separator} </> : ' ')}
<Kbd className={chipClass}>{k}</Kbd>
</Fragment>
))}
{children != null && <> {children}</>}
</>
);
if (!onClick) {
return (
<span className={className} {...rest}>
{content}
</span>
);
}
return (
<button
type="button"
onClick={onClick}
// Keep focus where it is (e.g. the compose textarea); the click still fires.
onMouseDown={(e) => e.preventDefault()}
title={title}
className={cn(
'cursor-pointer rounded transition-colors hover:text-white/80',
className,
)}
{...rest}
>
{content}
</button>
);
}
@@ -1,6 +1,7 @@
import { useSuspendPlayback } from '@/hooks/use-suspend-playback';
import { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { KeyHint } from '@/components/key-hint';
export interface KeybindingEntry {
keys: string[];
@@ -54,16 +55,15 @@ export function KeybindingsOverlay({
<div className="absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur-xl">
<div className="mb-5 flex items-center justify-between">
<h2 className="text-sm font-semibold text-white/70">{title}</h2>
<span className="text-xs text-white/30">
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Esc
</kbd>{' '}
or{' '}
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
?
</kbd>{' '}
<KeyHint
keys={['Esc', '?']}
separator="or"
onClick={onClose}
title="Close (or press Esc / ?)"
className="text-xs text-white/30"
>
to close
</span>
</KeyHint>
</div>
<div className="flex flex-col gap-5">
{groups.map((group) => (
@@ -80,16 +80,10 @@ export function KeybindingsOverlay({
<span className="text-sm text-white/70">
{binding.description}
</span>
<span className="flex items-center gap-1">
{binding.keys.map((k) => (
<kbd
key={k}
className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs text-white/60"
>
{k}
</kbd>
))}
</span>
<KeyHint
keys={binding.keys}
className="flex items-center gap-1 text-white/60"
/>
</li>
))}
</ul>
+26
View File
@@ -0,0 +1,26 @@
import { cn } from '@/lib/utils';
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
return (
<kbd
data-slot="kbd"
className={cn(
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
className,
)}
{...props}
/>
);
}
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<kbd
data-slot="kbd-group"
className={cn('inline-flex items-center gap-1', className)}
{...props}
/>
);
}
export { Kbd, KbdGroup };
@@ -1,4 +1,5 @@
import { Video, Mic } from 'lucide-react';
import { KeyHint } from '@/components/key-hint';
import { useMediaSettingsStore } from '@/stores/media-settings-store';
export function VideoAudioToggle() {
@@ -6,8 +7,8 @@ export function VideoAudioToggle() {
const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode);
return (
<span
role="button"
<KeyHint
keys="V"
onClick={() =>
setRecordingMode(recordingMode === 'video' ? 'audio' : 'video')
}
@@ -16,11 +17,7 @@ export function VideoAudioToggle() {
? 'Switch to audio-only (V)'
: 'Switch to video (V)'
}
className="cursor-pointer transition-colors hover:text-white/80"
>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
V
</kbd>{' '}
{recordingMode === 'video' ? (
<>
<Video className="inline size-3" /> video
@@ -30,6 +27,6 @@ export function VideoAudioToggle() {
<Mic className="inline size-3" /> audio
</>
)}
</span>
</KeyHint>
);
}