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:
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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user