From 023e332e011714c127bba86ea85b1fada81d43fc Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Thu, 11 Jun 2026 10:25:01 -0700 Subject: [PATCH] allow clicking keyboard hints --- .../confirm-destructive-overlay.tsx | 13 +-- js/desktop/src/components/key-hint.tsx | 79 +++++++++++++++++++ .../src/components/keybindings-overlay.tsx | 32 +++----- js/desktop/src/components/ui/kbd.tsx | 26 ++++++ .../src/components/video-audio-toggle.tsx | 11 +-- .../attachments/attachment-lightbox.tsx | 52 +++++++----- .../src/features/compose/compose-overlay.tsx | 21 ++--- .../compose/configure-stream-step.tsx | 19 +++-- .../features/compose/recording-overlay.tsx | 53 ++++--------- .../src/features/compose/text-editor.tsx | 32 ++++---- js/desktop/src/features/network-root.tsx | 38 +++------ .../particles/rename-stream-overlay.tsx | 13 +-- .../particles/stream-members-overlay.tsx | 13 +-- .../src/features/particles/stream-view.tsx | 65 +++++++-------- 14 files changed, 268 insertions(+), 199 deletions(-) create mode 100644 js/desktop/src/components/key-hint.tsx create mode 100644 js/desktop/src/components/ui/kbd.tsx diff --git a/js/desktop/src/components/confirm-destructive-overlay.tsx b/js/desktop/src/components/confirm-destructive-overlay.tsx index cf57b0f..8e19909 100644 --- a/js/desktop/src/components/confirm-destructive-overlay.tsx +++ b/js/desktop/src/components/confirm-destructive-overlay.tsx @@ -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({

{title}

- - - Esc - {' '} + to close - +
{description}
diff --git a/js/desktop/src/components/key-hint.tsx b/js/desktop/src/components/key-hint.tsx new file mode 100644 index 0000000..d7e4692 --- /dev/null +++ b/js/desktop/src/components/key-hint.tsx @@ -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 + ); +} diff --git a/js/desktop/src/components/keybindings-overlay.tsx b/js/desktop/src/components/keybindings-overlay.tsx index 18e1e81..7270b18 100644 --- a/js/desktop/src/components/keybindings-overlay.tsx +++ b/js/desktop/src/components/keybindings-overlay.tsx @@ -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({

{title}

- - - Esc - {' '} - or{' '} - - ? - {' '} + to close - +
{groups.map((group) => ( @@ -80,16 +80,10 @@ export function KeybindingsOverlay({ {binding.description} - - {binding.keys.map((k) => ( - - {k} - - ))} - + ))} diff --git a/js/desktop/src/components/ui/kbd.tsx b/js/desktop/src/components/ui/kbd.tsx new file mode 100644 index 0000000..f9d87d8 --- /dev/null +++ b/js/desktop/src/components/ui/kbd.tsx @@ -0,0 +1,26 @@ +import { cn } from '@/lib/utils'; + +function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) { + return ( + + ); +} + +function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ); +} + +export { Kbd, KbdGroup }; diff --git a/js/desktop/src/components/video-audio-toggle.tsx b/js/desktop/src/components/video-audio-toggle.tsx index 9ad3ad6..710fe25 100644 --- a/js/desktop/src/components/video-audio-toggle.tsx +++ b/js/desktop/src/components/video-audio-toggle.tsx @@ -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 ( - 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" > - - V - {' '} {recordingMode === 'video' ? ( <> + ); } diff --git a/js/desktop/src/features/attachments/attachment-lightbox.tsx b/js/desktop/src/features/attachments/attachment-lightbox.tsx index 85a57bc..f46b8c9 100644 --- a/js/desktop/src/features/attachments/attachment-lightbox.tsx +++ b/js/desktop/src/features/attachments/attachment-lightbox.tsx @@ -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({
{hasMultiple && ( - - ← - - - → - + goTo(-1)} + title="Previous (or press ←)" + aria-label="Previous attachment" + /> + goTo(1)} + title="Next (or press →)" + aria-label="Next attachment" + /> navigate )} {canDownload && ( - - - D - + download - + )} {onRemove && ( - - - ⌫ - + remove - + )} - - - Esc - + onOpenChange(null)} + title="Close (or press Esc)" + > close - +
)} diff --git a/js/desktop/src/features/compose/compose-overlay.tsx b/js/desktop/src/features/compose/compose-overlay.tsx index ef46124..c8e240a 100644 --- a/js/desktop/src/features/compose/compose-overlay.tsx +++ b/js/desktop/src/features/compose/compose-overlay.tsx @@ -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({
- - +
)} diff --git a/js/desktop/src/features/compose/configure-stream-step.tsx b/js/desktop/src/features/compose/configure-stream-step.tsx index dce7488..3370cf6 100644 --- a/js/desktop/src/features/compose/configure-stream-step.tsx +++ b/js/desktop/src/features/compose/configure-stream-step.tsx @@ -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 */}
- - - Esc - {' '} + cancel - - - - {metaKey}+Enter - {' '} + + create - +
); diff --git a/js/desktop/src/features/compose/recording-overlay.tsx b/js/desktop/src/features/compose/recording-overlay.tsx index d720b1f..0022a78 100644 --- a/js/desktop/src/features/compose/recording-overlay.tsx +++ b/js/desktop/src/features/compose/recording-overlay.tsx @@ -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 { @@ -208,33 +209,22 @@ export function RecordingOverlay({ {/* Bottom center: keyboard hints */} {isRecording && !isLoading && (
- - +
)} @@ -250,32 +240,21 @@ export function RecordingOverlay({ )}
- - + - +
); } diff --git a/js/desktop/src/features/particles/rename-stream-overlay.tsx b/js/desktop/src/features/particles/rename-stream-overlay.tsx index d82d0aa..c05b28e 100644 --- a/js/desktop/src/features/particles/rename-stream-overlay.tsx +++ b/js/desktop/src/features/particles/rename-stream-overlay.tsx @@ -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({

Rename stream

- - - Esc - {' '} + to close - +

Members

- - - Esc - {' '} + to close - +
{/* Visibility */} diff --git a/js/desktop/src/features/particles/stream-view.tsx b/js/desktop/src/features/particles/stream-view.tsx index 3416b21..4f10acb 100644 --- a/js/desktop/src/features/particles/stream-view.tsx +++ b/js/desktop/src/features/particles/stream-view.tsx @@ -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, @@ -362,6 +363,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) { setShowKeybindings(true)} + onOpenHuddle={handleOpenHuddle} /> setShowKeybindings(true)} + onOpenHuddle={handleOpenHuddle} /> ; exitRemainingMs: number | null; onOpenKeybindings: () => void; + onOpenHuddle: () => void; }) { return (
@@ -590,58 +596,53 @@ function BottomBar({ function StreamViewControls({ showEscape, onOpenKeybindings, + onOpenHuddle, }: { showEscape?: boolean; onOpenKeybindings: () => void; + onOpenHuddle: () => void; }) { + const navigate = useNavigate(); const requestIntent = useComposeIntentStore((s) => s.request); return (
{showEscape && ( - - - Esc - {' '} + navigate(-1)} + title="Back to network (or press Esc)" + > back - + )} - - - - - H - {' '} - huddle - - + - ? - + huddle + +
); }