diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index cc7c0b5..4d0b396 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -36,8 +36,16 @@ const OnboardingOverlay = lazy(() => import("./components/OnboardingOverlay")); const DEMO_MODE = import.meta.env.VITE_DEMO_MODE === "true"; const SESSION_TOKEN_KEY = "understand-anything-token"; +const ONBOARDING_DISMISSED_KEY = "ua-onboarding-dismissed-v1"; type SidebarTab = "info" | "files"; +function shouldShowOnboarding(): boolean { + if (typeof window === "undefined") return false; + const params = new URLSearchParams(window.location.search); + if (params.get("onboard") === "force") return true; + return window.localStorage.getItem(ONBOARDING_DISMISSED_KEY) !== "1"; +} + /** Resolve data file URL — in demo mode, use env var URLs; otherwise use local paths with token. */ function dataUrl(fileName: string, token: string | null): string { if (DEMO_MODE) { @@ -236,6 +244,13 @@ function DashboardContent({ const toggleShowFunctionsInClassView = useDashboardStore((s) => s.toggleShowFunctionsInClassView); const [showKeyboardHelp, setShowKeyboardHelp] = useState(false); const [sidebarTab, setSidebarTab] = useState("info"); + const [showOnboarding, setShowOnboarding] = useState(shouldShowOnboarding); + const dismissOnboarding = useCallback((remember: boolean) => { + if (remember && typeof window !== "undefined") { + window.localStorage.setItem(ONBOARDING_DISMISSED_KEY, "1"); + } + setShowOnboarding(false); + }, []); const viewMode = useDashboardStore((s) => s.viewMode); const setViewMode = useDashboardStore((s) => s.setViewMode); const isKnowledgeGraph = useDashboardStore((s) => s.isKnowledgeGraph); @@ -685,10 +700,12 @@ function DashboardContent({ )} - {/* First-visit onboarding overlay — auto-hides after dismiss via localStorage. */} - - - + {/* First-visit onboarding overlay — only mounted when needed so its chunk is lazy-loaded on demand. */} + {showOnboarding && ( + + + + )} ); } diff --git a/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx b/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx index 71b968c..a377a12 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/OnboardingOverlay.tsx @@ -1,53 +1,59 @@ -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import { useI18n } from "../contexts/I18nContext"; /** - * First-visit onboarding overlay. + * First-visit onboarding overlay (controlled). * - * Renders 5 dismissible steps that teach a new user how to operate the dashboard. - * State persists in localStorage so returning users are not interrupted. + * Parent owns the visibility + persistence state (see App.tsx). This component + * only renders the modal and reports the user's intent via onDismiss: + * - onDismiss(true) → "Skip" / Finish — parent should persist. + * - onDismiss(false) → backdrop click / Escape — parent should close without persisting. * - * Force-show via `?onboard=force` URL param (useful for screenshots / demos). + * Force-show is handled by the parent (see `shouldShowOnboarding` in App.tsx). */ -const STORAGE_KEY = "ua-onboarding-dismissed-v1"; +interface Props { + onDismiss: (remember: boolean) => void; +} -export default function OnboardingOverlay() { +const TITLE_ID = "ua-onboarding-title"; + +export default function OnboardingOverlay({ onDismiss }: Props) { const { t } = useI18n(); const STEPS = t.onboarding.steps; const [stepIdx, setStepIdx] = useState(0); - const [open, setOpen] = useState(false); + // Capture-phase Escape handler — runs before the global keydown chain so we + // can stopPropagation() and prevent it from also firing. useEffect(() => { - if (typeof window === "undefined") return; - const params = new URLSearchParams(window.location.search); - const force = params.get("onboard") === "force"; - const dismissed = window.localStorage.getItem(STORAGE_KEY) === "1"; - if (force || !dismissed) setOpen(true); - }, []); - - if (!open) return null; + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.stopPropagation(); + onDismiss(false); + } + }; + document.addEventListener("keydown", handler, true); + return () => document.removeEventListener("keydown", handler, true); + }, [onDismiss]); const isFirst = stepIdx === 0; const isLast = stepIdx === STEPS.length - 1; const step = STEPS[stepIdx]; - function dismiss(remember: boolean) { - if (remember && typeof window !== "undefined") { - window.localStorage.setItem(STORAGE_KEY, "1"); - } - setOpen(false); - } - return (
{ - if (e.target === e.currentTarget) dismiss(false); + if (e.target === e.currentTarget) onDismiss(false); }} > -
+
0{stepIdx + 1} / 0{STEPS.length} @@ -55,11 +61,13 @@ export default function OnboardingOverlay() { {t.onboarding.header}
-

{step.title}

+

+ {step.title} +

{step.body}

{step.hint && (
- · + · {step.hint}
)} @@ -70,7 +78,10 @@ export default function OnboardingOverlay() { key={i} style={{ ...dotProgressStyle, - background: i === stepIdx ? "#c8a882" : "#444", + background: + i === stepIdx + ? "var(--color-accent)" + : "var(--color-border-medium)", width: i === stepIdx ? 28 : 6, }} /> @@ -80,7 +91,7 @@ export default function OnboardingOverlay() {