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
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>
);
}
@@ -12,6 +12,7 @@ import {
import { useDownloadUrl } from '@/hooks/use-download-url';
import { useObjectUrl } from '@/hooks/use-object-url';
import { Button } from '@/components/ui/button';
import { KeyHint } from '@/components/key-hint';
import { useSuspendPlayback } from '@/hooks/use-suspend-playback';
import { platform } from '@/lib/platform';
@@ -294,37 +295,46 @@ export function AttachmentLightbox({
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 items-center gap-4 text-xs text-white/50">
{hasMultiple && (
<span className="flex items-center gap-1.5">
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
</kbd>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
</kbd>
<KeyHint
keys="←"
onClick={() => goTo(-1)}
title="Previous (or press ←)"
aria-label="Previous attachment"
/>
<KeyHint
keys="→"
onClick={() => goTo(1)}
title="Next (or press →)"
aria-label="Next attachment"
/>
navigate
</span>
)}
{canDownload && (
<span className="flex items-center gap-1.5">
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
D
</kbd>
<KeyHint
keys="D"
onClick={handleDownload}
title="Download (or press D)"
>
download
</span>
</KeyHint>
)}
{onRemove && (
<span className="flex items-center gap-1.5">
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
</kbd>
<KeyHint
keys="⌫"
onClick={handleRemove}
title="Remove (or press Backspace)"
>
remove
</span>
</KeyHint>
)}
<span className="flex items-center gap-1.5">
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Esc
</kbd>
<KeyHint
keys="Esc"
onClick={() => onOpenChange(null)}
title="Close (or press Esc)"
>
close
</span>
</KeyHint>
</div>
</>
)}
@@ -17,6 +17,7 @@ import { particlePath, parseParticlePath } from '@/lib/particle-path';
import type { ParticlePath } from '@/lib/particle-path';
import { RecordingOverlay } from '@/features/compose/recording-overlay';
import { ScreenSourcePicker } from '@/components/screen-source-picker';
import { KeyHint } from '@/components/key-hint';
import { TextComposeStep } from '@/features/compose/text-compose-step';
import { ConfigureStreamStep } from '@/features/compose/configure-stream-step';
import { apiClient } from '@/api/client';
@@ -726,28 +727,20 @@ export function ComposeOverlay({
</div>
</div>
<div className="absolute bottom-4 z-10 flex items-center gap-4 text-sm text-white/50">
<button
type="button"
<KeyHint
keys="S"
onClick={handleStopIntent}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Stop screen recording (or press S)"
>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
S
</kbd>{' '}
stop
</button>
<button
type="button"
</KeyHint>
<KeyHint
keys="Q"
onClick={handleCancelIntent}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Cancel screen recording (or press Q)"
>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Q
</kbd>{' '}
cancel
</button>
</KeyHint>
</div>
</div>
)}
@@ -5,6 +5,7 @@ import { metaKey } from '@/lib/platform';
import { useAuthStore } from '@/stores/auth-store';
import { generateRandomName } from '@/lib/random-name';
import { Input } from '@/components/ui/input';
import { KeyHint } from '@/components/key-hint';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { ScrollArea } from '@/components/ui/scroll-area';
@@ -158,18 +159,16 @@ export function ConfigureStreamStep({
{/* Keyboard hints */}
<div className="absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm text-white/50">
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Esc
</kbd>{' '}
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
cancel
</span>
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
{metaKey}+Enter
</kbd>{' '}
</KeyHint>
<KeyHint
keys={`${metaKey}+Enter`}
onClick={handleSubmit}
title={`Create stream (or press ${metaKey}+Enter)`}
>
create
</span>
</KeyHint>
</div>
</div>
);
@@ -8,6 +8,7 @@ import { AttachmentStrip } from '@/features/compose/attachment-strip';
import type { PendingAttachment } from '@/features/compose/attachment-strip';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { KeyHint } from '@/components/key-hint';
import { useComposeIntentStore } from '@/stores/compose-intent-store';
interface RecordingOverlayProps {
@@ -205,33 +206,22 @@ export function RecordingOverlay({
{/* Bottom center: keyboard hints */}
{isRecording && !isLoading && (
<div className="absolute bottom-4 z-10 flex items-center gap-4 text-sm text-white/50">
<button
type="button"
<KeyHint
keys="`"
prefix="Release"
onClick={() => requestIntent('stop')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Finish recording (or release `)"
>
Release{' '}
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
`
</kbd>{' '}
to review
</button>
<button
type="button"
</KeyHint>
<KeyHint
keys={['Esc', 'Q']}
separator="or"
onClick={() => requestIntent('cancel')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Discard recording (or press Esc / Q)"
>
<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">
Q
</kbd>{' '}
to cancel
</button>
</KeyHint>
</div>
)}
@@ -247,32 +237,21 @@ export function RecordingOverlay({
</div>
)}
<div className="flex items-center gap-4 text-sm text-white/50">
<button
type="button"
<KeyHint
keys="Enter"
onClick={() => requestIntent('send')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Send (or press Enter)"
>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Enter
</kbd>{' '}
next
</button>
<button
type="button"
</KeyHint>
<KeyHint
keys={['Esc', 'Q']}
separator="or"
onClick={() => requestIntent('cancel')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Discard (or press Esc / Q)"
>
<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">
Q
</kbd>{' '}
to cancel
</button>
</KeyHint>
<span>
<Button
variant="ghost"
+17 -15
View File
@@ -6,6 +6,7 @@ import { useAllLinkMetadata } from '@/hooks/use-link-metadata';
import { AttachmentStrip } from '@/features/compose/attachment-strip';
import type { PendingAttachment } from '@/features/compose/attachment-strip';
import { Button } from '@/components/ui/button';
import { KeyHint } from '@/components/key-hint';
import { MarkdownEditor } from '@/features/compose/markdown-editor';
export interface TextEditorAttachmentProps {
@@ -111,25 +112,26 @@ export function TextEditor({
const keyboardHints = (
<div className="absolute bottom-4 flex items-center gap-4 text-sm text-white/50">
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Esc
</kbd>{' '}
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
cancel
</span>
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
{metaKey}+Enter
</kbd>{' '}
</KeyHint>
<KeyHint
keys={`${metaKey}+Enter`}
onClick={() => {
if (textContent.trim()) onSubmit();
}}
title={`Submit (or press ${metaKey}+Enter)`}
>
{submitHint}
</span>
</KeyHint>
{immersive && (
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
{metaKey}+M
</kbd>{' '}
<KeyHint
keys={`${metaKey}+M`}
onClick={() => setForceCardMode(true)}
title={`Switch to markdown editor (or press ${metaKey}+M)`}
>
markdown
</span>
</KeyHint>
)}
{attachmentProps && (
<span>
+9 -29
View File
@@ -4,6 +4,7 @@ import { CircleDot, CircleCheckBig } from 'lucide-react';
import { particlePath } from '@/lib/particle-path';
import { ParticleListView } from '@/features/particles/particle-list-view';
import { VideoAudioToggle } from '@/components/video-audio-toggle';
import { KeyHint } from '@/components/key-hint';
import { ComposeOverlay } from './compose/compose-overlay';
import { useComposeIntentStore } from '@/stores/compose-intent-store';
import { ComposeQuotaIndicator } from './compose/compose-quota-indicator';
@@ -103,44 +104,23 @@ function NetworkRootControls() {
const requestIntent = useComposeIntentStore((s) => s.request);
return (
<div className="flex items-center gap-4 text-sm text-white/50">
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
</kbd>{' '}
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Enter
</kbd>{' '}
navigate
</span>
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
19
</kbd>{' '}
jump
</span>
<KeyHint keys={['↑↓', 'Enter']}>navigate</KeyHint>
<KeyHint keys="19">jump</KeyHint>
<VideoAudioToggle />
<button
type="button"
<KeyHint
keys="Hold `"
onClick={() => requestIntent('record')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Start recording (or hold `)"
>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Hold `
</kbd>{' '}
to start
</button>
<button
type="button"
</KeyHint>
<KeyHint
keys="T"
onClick={() => requestIntent('text')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Compose text (or press T)"
>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
T
</kbd>{' '}
text
</button>
</KeyHint>
</div>
);
}
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { KeyHint } from '@/components/key-hint';
import { updateParticleProperties } from '@/lib/firestore-particles';
import { particlePath, toFirestoreDocPath } from '@/lib/particle-path';
import type { Particle } from '@/api/types';
@@ -63,12 +64,14 @@ export function RenameStreamOverlay({
<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-4 flex items-center justify-between">
<h2 className="text-sm font-semibold text-white/70">Rename stream</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>
<Input
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
import { X, UserPlus, Globe, Users, Lock } from 'lucide-react';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { ScrollArea } from '@/components/ui/scroll-area';
import { KeyHint } from '@/components/key-hint';
import {
buildCustomVisibility,
buildNetworkVisibility,
@@ -104,12 +105,14 @@ export function StreamMembersOverlay({
{/* Header */}
<div className="mb-4 flex items-center justify-between">
<h2 className="text-sm font-semibold text-white/70">Members</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>
{/* Visibility */}
@@ -29,6 +29,7 @@ import { TextParticleView } from '@/features/particles/text-particle-view';
import { FallbackParticleView } from '@/features/particles/fallback-particle-view';
import { DeletedParticleView } from '@/features/particles/deleted-particle-view';
import { VideoAudioToggle } from '@/components/video-audio-toggle';
import { KeyHint } from '@/components/key-hint';
import { useMediaSettingsStore } from '@/stores/media-settings-store';
import {
KeybindingsOverlay,
@@ -259,12 +260,17 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
const { fastPlayback } = usePlaybackKeys({ mediaRef });
const handleExitNavigate = useCallback(() => {
navigate(`/${networkId}`);
}, [navigate, networkId]);
useStreamNavigationKeys({
next,
prev,
currentIndex,
childrenLength: children.length,
mediaRef,
onExit: handleExitNavigate,
});
const handleOpenHuddle = useCallback(() => {
@@ -325,10 +331,6 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
// Always show controls when compose is active or exit countdown is visible
const controlsVisible = showControls || composeActive || status === 'ended';
const handleExitNavigate = useCallback(() => {
navigate(`/${networkId}`);
}, [navigate, networkId]);
const exitRemainingMs = useExitCountdown(status, paused, handleExitNavigate);
// Reset progress when the particle changes.
@@ -362,6 +364,8 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
<StreamViewControls
showEscape
onOpenKeybindings={() => setShowKeybindings(true)}
onOpenHuddle={handleOpenHuddle}
onExit={handleExitNavigate}
/>
<ComposeOverlay
networkId={networkId}
@@ -505,6 +509,8 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
onlineHumanIds={onlineHumanIds}
exitRemainingMs={exitRemainingMs}
onOpenKeybindings={() => setShowKeybindings(true)}
onOpenHuddle={handleOpenHuddle}
onExit={handleExitNavigate}
/>
<KeybindingsOverlay
@@ -527,6 +533,8 @@ function BottomBar({
onlineHumanIds,
exitRemainingMs,
onOpenKeybindings,
onOpenHuddle,
onExit,
}: {
visible: boolean;
total: number;
@@ -540,6 +548,8 @@ function BottomBar({
onlineHumanIds: Set<string>;
exitRemainingMs: number | null;
onOpenKeybindings: () => void;
onOpenHuddle: () => void;
onExit: () => void;
}) {
return (
<div
@@ -580,6 +590,8 @@ function BottomBar({
<StreamViewControls
showEscape
onOpenKeybindings={onOpenKeybindings}
onOpenHuddle={onOpenHuddle}
onExit={onExit}
/>
</div>
</div>
@@ -590,58 +602,54 @@ function BottomBar({
function StreamViewControls({
showEscape,
onOpenKeybindings,
onOpenHuddle,
onExit,
}: {
showEscape?: boolean;
onOpenKeybindings: () => void;
onOpenHuddle: () => void;
onExit: () => void;
}) {
const requestIntent = useComposeIntentStore((s) => s.request);
return (
<div className="flex items-center gap-4 text-sm text-white/50">
{showEscape && (
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Esc
</kbd>{' '}
<KeyHint
keys="Esc"
onClick={onExit}
title="Back to network (or press Esc)"
>
back
</span>
</KeyHint>
)}
<VideoAudioToggle />
<button
type="button"
<KeyHint
keys="Hold `"
onClick={() => requestIntent('record')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Reply with a recording (or hold `)"
>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
Hold `
</kbd>{' '}
to reply
</button>
<button
type="button"
</KeyHint>
<KeyHint
keys="T"
onClick={() => requestIntent('text')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Reply with text (or press T)"
>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
T
</kbd>{' '}
text
</button>
<span>
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
H
</kbd>{' '}
huddle
</span>
<kbd
role="button"
onClick={onOpenKeybindings}
className="cursor-pointer rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs transition-colors hover:text-white/80"
title="Show all shortcuts"
</KeyHint>
<KeyHint
keys="H"
onClick={onOpenHuddle}
title="Start a huddle (or press H)"
>
?
</kbd>
huddle
</KeyHint>
<KeyHint
keys="?"
onClick={onOpenKeybindings}
title="Show all shortcuts"
aria-label="Show keyboard shortcuts"
/>
</div>
);
}
@@ -1,5 +1,4 @@
import { useEffect, type RefObject } from 'react';
import { useNavigate } from 'react-router-dom';
import type { MediaParticleHandle } from '@/features/particles/media-particle-view';
import { isTypingTarget } from '@/lib/keyboard';
@@ -11,6 +10,7 @@ interface UseStreamNavigationKeysOptions {
currentIndex: number;
childrenLength: number;
mediaRef: RefObject<MediaParticleHandle | null>;
onExit: () => void;
}
/**
@@ -23,9 +23,8 @@ export function useStreamNavigationKeys({
currentIndex,
childrenLength,
mediaRef,
onExit,
}: UseStreamNavigationKeysOptions) {
const navigate = useNavigate();
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (isTypingTarget(e)) return;
@@ -53,12 +52,12 @@ export function useStreamNavigationKeys({
break;
case 'Escape':
e.preventDefault();
navigate(-1);
onExit();
break;
}
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [next, prev, currentIndex, childrenLength, mediaRef, navigate]);
}, [next, prev, currentIndex, childrenLength, mediaRef, onExit]);
}