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