Files
llink/js/desktop/src/features/onboarding/use-tap-gesture.ts
T
ClaudeandArjun Patel af06a250fe Add first-run onboarding wizard for recording mechanics
Introduce a minimal, skippable first-run wizard that teaches the record
key. On first authenticated launch it renders as a full-screen takeover
(gated above PusherProvider) so the stream/compose keyboard handlers are
not mounted to compete for the same keys.

Steps: welcome, hold ` to record, tap V to switch audio/video, tap ` to
toggle recording, mic/camera permission priming, and a recap. The lessons
detect the real gestures and never block — Esc or the Skip control
dismisses at any point.

Reuses existing pieces: VideoAudioToggle, useMediaDevices().requestLabels
for permission priming, KeyHint, and isTypingTarget. Extracts the shared
HOLD_THRESHOLD_MS into lib/constants and reuses it in the compose overlay.

Resolves #259

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01A9AHySdLb1zfZAEm6SuaQP
2026-06-21 11:35:01 -07:00

53 lines
1.5 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { isTypingTarget } from '@/lib/keyboard';
interface UseTapGestureOptions {
/** Detected against KeyboardEvent.key. */
targetKey: string;
/** Optional KeyboardEvent.code fallback (e.g. 'Backquote'). */
targetCode?: string;
/** Taps required before `satisfied` flips true. Defaults to 1. */
taps?: number;
/** Fires on each counted tap, with the new running count. */
onTap?: (count: number) => void;
}
/**
* Counts discrete taps of a single key, ignoring auto-repeat. Used both for the
* one-tap audio/video toggle and the two-tap start/stop demonstration.
*/
export function useTapGesture({
targetKey,
targetCode,
taps = 1,
onTap,
}: UseTapGestureOptions) {
const [count, setCount] = useState(0);
const onTapRef = useRef(onTap);
useEffect(() => {
onTapRef.current = onTap;
});
useEffect(() => {
const matches = (e: KeyboardEvent) =>
e.key === targetKey || (targetCode != null && e.code === targetCode);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.repeat || isTypingTarget(e) || !matches(e)) return;
e.preventDefault();
setCount((prev) => {
if (prev >= taps) return prev;
const next = prev + 1;
onTapRef.current?.(next);
return next;
});
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [targetKey, targetCode, taps]);
return { taps: count, satisfied: count >= taps };
}