From 8d79dd93b8093ade3c67132d34dbf64a0db708a0 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Wed, 13 May 2026 15:48:21 -0400 Subject: [PATCH] feat(dashboard): focus same-page Start Session input from NextStep CTA and checklist The "Start a session" CTAs on the NextStepCard and SetupChecklist used to Link-navigate, which left the user on the same page (the Start Session input lives on the dashboard) without any visible response. Replace those CTAs with a custom window-event dispatch (FOCUS_START_SESSION_EVENT) that the StartSessionInput listens for: scroll the input into view, focus the textarea, and pulse a ring for 900ms so the click feels intentional. The NextStepCard also locally hides itself after firing so the user isn't double-prompted while typing. Co-Authored-By: Claude Opus 4.7 --- .../src/components/dashboard/NextStepCard.tsx | 35 ++++++++++++++----- .../components/dashboard/SetupChecklist.tsx | 16 +++++++++ .../dashboard/StartSessionInput.tsx | 28 +++++++++++++-- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/dashboard/NextStepCard.tsx b/frontend/src/components/dashboard/NextStepCard.tsx index 5d54a266..9b158d5a 100644 --- a/frontend/src/components/dashboard/NextStepCard.tsx +++ b/frontend/src/components/dashboard/NextStepCard.tsx @@ -6,6 +6,7 @@ import type { OnboardingStatus } from '@/api/onboarding' import { useTrialBanner } from '@/hooks/useTrialBanner' import type { TrialBannerStage } from '@/hooks/useTrialBanner' import { useOnboardingStatus } from '@/hooks/useOnboardingStatus' +import { FOCUS_START_SESSION_EVENT } from '@/components/dashboard/StartSessionInput' /** * Next-step card — surfaces the single highest-priority incomplete onboarding @@ -114,9 +115,10 @@ export function pickNextStep( export function NextStepCard() { const status = useOnboardingStatus() const [locallyDismissed, setLocallyDismissed] = useState(false) + const [locallyHidden, setLocallyHidden] = useState(false) const { stage } = useTrialBanner() - if (!status || status.dismissed || locallyDismissed) return null + if (!status || status.dismissed || locallyDismissed || locallyHidden) return null const next = pickNextStep(status, stage) if (!next) return null @@ -154,14 +156,29 @@ export function NextStepCard() {
- - {next.ctaLabel} - - + {next.key === 'ran_session' ? ( + + ) : ( + + {next.ctaLabel} + + + )}
) diff --git a/frontend/src/components/dashboard/SetupChecklist.tsx b/frontend/src/components/dashboard/SetupChecklist.tsx index 7d8677b2..13ddcb1f 100644 --- a/frontend/src/components/dashboard/SetupChecklist.tsx +++ b/frontend/src/components/dashboard/SetupChecklist.tsx @@ -5,6 +5,7 @@ import type { OnboardingStatus } from '@/api/onboarding' import { useTrialBanner } from '@/hooks/useTrialBanner' import type { TrialBannerStage } from '@/hooks/useTrialBanner' import { useOnboardingStatus } from '@/hooks/useOnboardingStatus' +import { FOCUS_START_SESSION_EVENT } from '@/components/dashboard/StartSessionInput' /** * Unified setup checklist — single list (no SOLO/TEAM bifurcation). @@ -112,6 +113,21 @@ export function SetupChecklist() { {item.label} + ) : item.key === 'ran_session' ? ( + ) : ( ([]) const [isDragOver, setIsDragOver] = useState(false) + const [nudge, setNudge] = useState(false) const navigate = useNavigate() + const wrapperRef = useRef(null) const textareaRef = useRef(null) const fileInputRef = useRef(null) const dragCounterRef = useRef(0) useEffect(() => { textareaRef.current?.focus() }, []) + // External "focus me" trigger (e.g. NextStepCard "Start a session" CTA on + // the same page). Scrolls into view, focuses the textarea, and pulses a + // ring so the click feels intentional even when the input was already + // partially visible. + useEffect(() => { + const handler = () => { + wrapperRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + textareaRef.current?.focus({ preventScroll: true }) + setNudge(true) + window.setTimeout(() => setNudge(false), 900) + } + window.addEventListener(FOCUS_START_SESSION_EVENT, handler) + return () => window.removeEventListener(FOCUS_START_SESSION_EVENT, handler) + }, []) + // Auto-grow textarea useEffect(() => { const el = textareaRef.current @@ -190,7 +209,8 @@ export function StartSessionInput() { return (
{/* Main input area */}
{/* Drag overlay */} {isDragOver && (