Files
llink/js/desktop/src/features/onboarding/toggle-step.tsx
T
Arjun Patel fec7f5d6d8 Add onboarding wizard for teaching the record key (#301)
* 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9AHySdLb1zfZAEm6SuaQP

* nits

* light mode support for onboarding

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-21 11:45:14 -07:00

61 lines
2.2 KiB
TypeScript

import { useEffect } from 'react';
import { Check, Mic, Video } from 'lucide-react';
import { OnboardingStep, SUCCESS_DWELL_MS } from './onboarding-step';
import { useTapGesture } from './use-tap-gesture';
import { VideoAudioToggle } from '@/components/video-audio-toggle';
import { useMediaSettingsStore } from '@/stores/media-settings-store';
export function ToggleStep({ onAdvance }: { onAdvance: () => void }) {
const recordingMode = useMediaSettingsStore((s) => s.recordingMode);
const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode);
const { satisfied } = useTapGesture({
targetKey: 'v',
onTap: () => {
const current = useMediaSettingsStore.getState().recordingMode;
setRecordingMode(current === 'video' ? 'audio' : 'video');
},
});
useEffect(() => {
if (!satisfied) return;
const t = setTimeout(onAdvance, SUCCESS_DWELL_MS);
return () => clearTimeout(t);
}, [satisfied, onAdvance]);
// Teaching the toggle shouldn't permanently change the user's default mode.
useEffect(() => {
const original = useMediaSettingsStore.getState().recordingMode;
return () => setRecordingMode(original);
}, [setRecordingMode]);
const isVideo = recordingMode === 'video';
return (
<OnboardingStep
title={satisfied ? 'Audio or video, your call' : 'Switch audio and video'}
subtitle={
satisfied
? 'Tap V before recording to choose how you show up.'
: 'Tap the V key to switch between video and audio.'
}
status={
satisfied ? (
<span className="inline-flex items-center gap-1.5 text-emerald-600 dark:text-emerald-300">
<Check className="size-4" />{' '}
{isVideo ? 'Back to video.' : 'Audio it is.'}
</span>
) : null
}
>
<div className="flex size-32 flex-col items-center justify-center gap-2 rounded-2xl border border-border bg-muted text-foreground">
{isVideo ? <Video className="size-9" /> : <Mic className="size-9" />}
<span className="text-sm text-muted-foreground">
{isVideo ? 'Video' : 'Audio'}
</span>
</div>
<VideoAudioToggle />
</OnboardingStep>
);
}