This commit is contained in:
Arjun Patel
2026-06-21 11:34:49 -07:00
parent af06a250fe
commit d62d35f25a
3 changed files with 46 additions and 54 deletions
@@ -29,7 +29,7 @@ function KeyCap({
<div
className={cn(
'relative flex select-none items-center justify-center overflow-hidden rounded-lg border text-sm font-medium',
hero ? 'h-14 w-14 text-xl' : 'h-11',
hero ? 'h-14 w-14 text-xl' : 'h-14',
!hero && 'border-white/10 bg-white/5 text-white/30',
hero &&
state === 'idle' &&
@@ -63,13 +63,13 @@ export function OnboardingKeyboard({
}: OnboardingKeyboardProps) {
return (
<div className="flex flex-col items-start gap-1.5">
<KeyCap label="esc" className="w-12 text-xs" />
<KeyCap label="esc" className="w-14 text-xs" />
<div className="flex gap-1.5">
<KeyCap label={highlightKey} hero state={state} progress={progress} />
<KeyCap label="1" className="w-11" />
<KeyCap label="1" className="w-14" />
</div>
<div className="flex gap-1.5">
<KeyCap label="tab" className="w-16 text-xs" />
<KeyCap label="tab" className="w-20 text-xs" />
<KeyCap label="Q" className="w-12" />
</div>
</div>
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { WindowControls } from '@/components/window-controls';
import { KeyHint } from '@/components/key-hint';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { OnboardingKeyboard } from './onboarding-keyboard';
import { OnboardingStep } from './onboarding-step';
@@ -10,14 +9,7 @@ import { ToggleStep } from './toggle-step';
import { TapStep } from './tap-step';
import { PermissionStep } from './permission-step';
const STEPS = [
'welcome',
'hold',
'toggle',
'tap',
'permission',
'done',
] as const;
const STEPS = ['welcome', 'hold', 'tap', 'toggle', 'permission'] as const;
type Step = (typeof STEPS)[number];
/**
@@ -34,6 +26,10 @@ export function OnboardingOverlay({ onComplete }: { onComplete: () => void }) {
setStep((cur) => STEPS[Math.min(STEPS.indexOf(cur) + 1, STEPS.length - 1)]);
}, []);
const goBack = useCallback(() => {
setStep((cur) => STEPS[Math.max(0, STEPS.indexOf(cur) - 1)]);
}, []);
// Focus the panel on the gesture steps so window-level key handling has a
// home; the static steps autofocus their primary button instead, which lets
// Enter activate it natively (no duplicate window handler).
@@ -43,17 +39,27 @@ export function OnboardingOverlay({ onComplete }: { onComplete: () => void }) {
}
}, [step]);
// Esc skips the wizard from anywhere.
// Esc skips the wizard from anywhere; ← (and Backspace) steps back.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onComplete();
} else if (e.key === 'Enter' && step === 'welcome') {
e.preventDefault();
goNext();
} else if (
e.key === 'ArrowLeft' ||
e.key === 'Backspace' ||
e.key === 'Delete'
) {
e.preventDefault();
goBack();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onComplete]);
}, [onComplete, goNext, step, goBack]);
return (
<div className="flex h-screen flex-col bg-background text-white">
@@ -80,17 +86,7 @@ export function OnboardingOverlay({ onComplete }: { onComplete: () => void }) {
{step === 'welcome' && (
<OnboardingStep
title="Welcome to llink"
subtitle="llink is built for your keyboard. Let's learn the one key that matters, it takes 20 seconds."
footer={
<>
<Button onClick={goNext} autoFocus>
Get started
</Button>
<KeyHint keys="Enter" className="text-xs text-white/40">
to begin
</KeyHint>
</>
}
subtitle="To use llink, you must learn the keyboard shortcuts. Don't worry, once you learn them, you'll feel like your flowing."
>
<OnboardingKeyboard highlightKey="`" state="idle" />
</OnboardingStep>
@@ -98,33 +94,7 @@ export function OnboardingOverlay({ onComplete }: { onComplete: () => void }) {
{step === 'hold' && <HoldStep onAdvance={goNext} />}
{step === 'toggle' && <ToggleStep onAdvance={goNext} />}
{step === 'tap' && <TapStep onAdvance={goNext} />}
{step === 'permission' && <PermissionStep onAdvance={goNext} />}
{step === 'done' && (
<OnboardingStep
title="You're all set"
subtitle="That's everything you need to start sharing."
footer={
<>
<Button onClick={onComplete} autoFocus>
Start using llink
</Button>
<KeyHint keys="?" className="text-xs text-white/40">
for all shortcuts
</KeyHint>
</>
}
>
<div className="flex flex-col items-center gap-2 text-sm text-white/70">
<KeyHint keys="`" prefix="Hold">
to talk
</KeyHint>
<KeyHint keys="V">switch audio / video</KeyHint>
<KeyHint keys="`" prefix="Tap">
to start and stop
</KeyHint>
</div>
</OnboardingStep>
)}
{step === 'permission' && <PermissionStep onAdvance={onComplete} />}
<div className="flex items-center justify-center gap-1.5">
{STEPS.map((s, i) => (
@@ -141,6 +111,28 @@ export function OnboardingOverlay({ onComplete }: { onComplete: () => void }) {
/>
))}
</div>
<div className="flex min-h-5 items-center justify-center gap-4 text-xs text-white/40">
{index > 0 && (
<KeyHint
keys="←"
onClick={goBack}
title="Back (or press ←)"
aria-label="Previous step"
>
back
</KeyHint>
)}
{step === 'welcome' && (
<KeyHint
keys="Enter"
onClick={goNext}
title="Continue (or press Enter)"
>
continue
</KeyHint>
)}
</div>
</div>
</div>
</div>
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
/** How long a success state lingers before the wizard auto-advances, in ms. */
export const SUCCESS_DWELL_MS = 800;
export const SUCCESS_DWELL_MS = 2000;
interface OnboardingStepProps {
title: ReactNode;