allow clicking keyboard hints
This commit is contained in:
@@ -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 > 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -208,33 +209,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>
|
||||
)}
|
||||
|
||||
@@ -250,32 +240,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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
1–9
|
||||
</kbd>{' '}
|
||||
jump
|
||||
</span>
|
||||
<KeyHint keys={['↑↓', 'Enter']}>navigate</KeyHint>
|
||||
<KeyHint keys="1–9">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,
|
||||
@@ -362,6 +363,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
<StreamViewControls
|
||||
showEscape
|
||||
onOpenKeybindings={() => setShowKeybindings(true)}
|
||||
onOpenHuddle={handleOpenHuddle}
|
||||
/>
|
||||
<ComposeOverlay
|
||||
networkId={networkId}
|
||||
@@ -505,6 +507,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
onlineHumanIds={onlineHumanIds}
|
||||
exitRemainingMs={exitRemainingMs}
|
||||
onOpenKeybindings={() => setShowKeybindings(true)}
|
||||
onOpenHuddle={handleOpenHuddle}
|
||||
/>
|
||||
|
||||
<KeybindingsOverlay
|
||||
@@ -527,6 +530,7 @@ function BottomBar({
|
||||
onlineHumanIds,
|
||||
exitRemainingMs,
|
||||
onOpenKeybindings,
|
||||
onOpenHuddle,
|
||||
}: {
|
||||
visible: boolean;
|
||||
total: number;
|
||||
@@ -540,6 +544,7 @@ function BottomBar({
|
||||
onlineHumanIds: Set<string>;
|
||||
exitRemainingMs: number | null;
|
||||
onOpenKeybindings: () => void;
|
||||
onOpenHuddle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -580,6 +585,7 @@ function BottomBar({
|
||||
<StreamViewControls
|
||||
showEscape
|
||||
onOpenKeybindings={onOpenKeybindings}
|
||||
onOpenHuddle={onOpenHuddle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<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={() => navigate(-1)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user