diff --git a/frontend/src/app/components/Onboarding/flow/OnboardingAtoms.tsx b/frontend/src/app/components/Onboarding/flow/OnboardingAtoms.tsx index a6eb2dc8..fa1817e0 100644 --- a/frontend/src/app/components/Onboarding/flow/OnboardingAtoms.tsx +++ b/frontend/src/app/components/Onboarding/flow/OnboardingAtoms.tsx @@ -1,54 +1,62 @@ -// Small shared UI atoms for the onboarding screens (serif heading, subtitle, white CTA, ghost link). +// Small shared UI atoms for the onboarding screens (serif heading, subtitle, CTA, ghost link). +// All theme-aware via useOnboardingSkin(). import React from 'react'; -import { ONBOARDING_SKIN as S } from './onboardingSkin'; +import { useOnboardingSkin } from './onboardingSkin'; -export const Heading: React.FC<{ children: React.ReactNode }> = ({ children }) => ( -

- {children} -

-); +export const Heading: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const S = useOnboardingSkin(); + return ( +

+ {children} +

+ ); +}; -export const Sub: React.FC<{ children: React.ReactNode }> = ({ children }) => ( -
{children}
-); +export const Sub: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const S = useOnboardingSkin(); + return
{children}
; +}; export const PrimaryButton: React.FC<{ onClick: () => void; children: React.ReactNode; style?: React.CSSProperties }> = ({ onClick, children, style, -}) => ( - -); +}) => { + const S = useOnboardingSkin(); + return ( + + ); +}; export const GhostLink: React.FC<{ onClick: () => void; children: React.ReactNode; style?: React.CSSProperties }> = ({ onClick, children, style, -}) => ( -
- {children} -
-); +}) => { + const S = useOnboardingSkin(); + return ( +
+ {children} +
+ ); +}; diff --git a/frontend/src/app/components/Onboarding/flow/OnboardingFlow.tsx b/frontend/src/app/components/Onboarding/flow/OnboardingFlow.tsx index 2d61e71c..d8ed48f7 100644 --- a/frontend/src/app/components/Onboarding/flow/OnboardingFlow.tsx +++ b/frontend/src/app/components/Onboarding/flow/OnboardingFlow.tsx @@ -18,14 +18,6 @@ import type { FlowStepId, PersonaId, PayoffIdea, IconName } from './onboardingFl // Icons for profile-generated ideas (the agent returns text, not icons); rotated for variety. const IDEA_ICONS: IconName[] = ['sun', 'tray', 'build', 'globe']; -const DOT_INDEX: Record = { - help: 0, - name: 1, - consent: 2, - connect: 3, - payoff: undefined, -}; - function greetingFor(name: string): string { const h = new Date().getHours(); const part = h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening'; @@ -108,9 +100,5 @@ export const OnboardingFlow: React.FC<{ onExit: () => void }> = ({ onExit }) => } }; - return ( - - {body()} - - ); + return {body()}; }; diff --git a/frontend/src/app/components/Onboarding/flow/OnboardingShell.tsx b/frontend/src/app/components/Onboarding/flow/OnboardingShell.tsx index f543564c..cd6a7647 100644 --- a/frontend/src/app/components/Onboarding/flow/OnboardingShell.tsx +++ b/frontend/src/app/components/Onboarding/flow/OnboardingShell.tsx @@ -1,53 +1,39 @@ -// The premium frame every onboarding screen sits in: brand pinned top-center, content -// vertically centered, entrance fade (reduced-motion aware), and the discovery progress dots. +// The premium frame every onboarding screen sits in: brand pinned top-center, content vertically +// centered, entrance fade (reduced-motion aware). Theme-aware. Portaled to body + near-max z-index +// so it escapes any ancestor transform's stacking context and covers ALL app chrome. import React from 'react'; import { createPortal } from 'react-dom'; import { motion } from 'framer-motion'; import { useReducedMotion } from '@/shared/hooks/useReducedMotion'; -import { ONBOARDING_SKIN as S, ONBOARDING_EASE } from './onboardingSkin'; +import { useOnboardingSkin, ONBOARDING_EASE } from './onboardingSkin'; import { Spark } from './OnboardingIcons'; interface Props { children: React.ReactNode; /** Changing this re-triggers the entrance fade (one per screen). */ stepKey: string; - /** 0-based; when set with totalSteps, renders the bottom dots. */ - stepIndex?: number; - totalSteps?: number; } -// Portaled to document.body + a near-max z-index so it escapes any ancestor transform's stacking -// context and covers ALL app chrome (the ⌘K search bar, headers). This is the real takeover layer. -const overlay: React.CSSProperties = { - position: 'fixed', - inset: 0, - zIndex: 2147483000, - background: S.bg, - color: S.text, - fontFamily: S.sans, - WebkitFontSmoothing: 'antialiased', - overflow: 'hidden', -}; - -export const OnboardingShell: React.FC = ({ children, stepKey, stepIndex, totalSteps }) => { +export const OnboardingShell: React.FC = ({ children, stepKey }) => { const reduce = useReducedMotion(); - const showDots = typeof stepIndex === 'number' && typeof totalSteps === 'number'; + const S = useOnboardingSkin(); if (typeof document === 'undefined') return null; return createPortal( -
-
+
+
@@ -73,22 +59,6 @@ export const OnboardingShell: React.FC = ({ children, stepKey, stepIndex, {children}
- - {showDots && ( -
- {Array.from({ length: totalSteps as number }).map((unused, i) => ( - - ))} -
- )}
, document.body, ); diff --git a/frontend/src/app/components/Onboarding/flow/Payoff.tsx b/frontend/src/app/components/Onboarding/flow/Payoff.tsx index b1819703..6dd31cae 100644 --- a/frontend/src/app/components/Onboarding/flow/Payoff.tsx +++ b/frontend/src/app/components/Onboarding/flow/Payoff.tsx @@ -1,13 +1,14 @@ // The payoff: name + quirky remark + insight + a prefilled runnable task + tailored "Ideas for you". -// Presentational; the orchestrator feeds it content (persona-derived now, profile-derived later). +// Presentational + theme-aware; the orchestrator feeds it content (persona floor or profile-derived). import React, { useState } from 'react'; -import { ONBOARDING_SKIN as S } from './onboardingSkin'; +import { useOnboardingSkin } from './onboardingSkin'; import { LineIcon } from './OnboardingIcons'; import { Heading } from './OnboardingAtoms'; import type { PayoffIdea } from './onboardingFlowTypes'; const IdeaRow: React.FC<{ idea: PayoffIdea; onPick: () => void }> = ({ idea, onPick }) => { + const S = useOnboardingSkin(); const [hover, setHover] = useState(false); return (
void; onPickIdea: (idea: PayoffIdea) => void; -}> = ({ greeting, remark, insight, prefilledPrompt, ideas, onRun, onPickIdea }) => ( - <> - {greeting} - {remark &&
{remark}
} -
{insight}
+}> = ({ greeting, remark, insight, prefilledPrompt, ideas, onRun, onPickIdea }) => { + const S = useOnboardingSkin(); + return ( + <> + {greeting} + {remark &&
{remark}
} +
{insight}
-
-
{prefilledPrompt}
- + - Cowork - Opus 4.8 - {prefilledPrompt}
+
- Run - + + + Cowork + Opus 4.8 + + Run + +
-
-
-
Ideas for you
- {ideas.map((idea) => ( - onPickIdea(idea)} /> - ))} -
- -); +
+
Ideas for you
+ {ideas.map((idea) => ( + onPickIdea(idea)} /> + ))} +
+ + ); +}; diff --git a/frontend/src/app/components/Onboarding/flow/onboardingSkin.ts b/frontend/src/app/components/Onboarding/flow/onboardingSkin.ts index 5cafc92d..0574d4a3 100644 --- a/frontend/src/app/components/Onboarding/flow/onboardingSkin.ts +++ b/frontend/src/app/components/Onboarding/flow/onboardingSkin.ts @@ -1,7 +1,29 @@ -// Always-dark premium skin for the onboarding flow, matching Claude.ai onboarding. -// Deliberately NOT the app's theme tokens: onboarding is always this dark, like Claude's. +// Premium onboarding skin (Claude-onboarding aesthetic) that follows the user's light/dark theme. +// Same layout/typography in both modes; only the palette flips. -export const ONBOARDING_SKIN = { +import { useThemeMode } from '@/shared/styles/ThemeContext'; + +export interface OnboardingSkin { + bg: string; + surface: string; + surfaceHover: string; + text: string; + muted: string; + ghost: string; + accent: string; + border: string; + borderStrong: string; + ctaBg: string; + ctaText: string; + serif: string; + sans: string; + radius: number; +} + +const SERIF = '"Copernicus", "Tiempos Headline", Georgia, "Times New Roman", serif'; +const SANS = '"Styrene B", "Anthropic Sans", -apple-system, "SF Pro Text", system-ui, sans-serif'; + +const DARK: OnboardingSkin = { bg: '#1C1B19', surface: '#252420', surfaceHover: '#2B2A25', @@ -13,10 +35,31 @@ export const ONBOARDING_SKIN = { borderStrong: 'rgba(243,241,234,0.14)', ctaBg: '#F3F1EA', ctaText: '#1A1917', - serif: '"Copernicus", "Tiempos Headline", Georgia, "Times New Roman", serif', - sans: '"Styrene B", "Anthropic Sans", -apple-system, "SF Pro Text", system-ui, sans-serif', + serif: SERIF, + sans: SANS, radius: 16, -} as const; +}; -// framer-motion cubic-bezier for entrances; matches the mock's --ease. +const LIGHT: OnboardingSkin = { + bg: '#F5F5F0', + surface: '#FFFFFF', + surfaceHover: '#F5F4ED', + text: '#1A1A18', + muted: '#73726C', + ghost: 'rgba(115,114,108,0.65)', + accent: '#C4633A', + border: 'rgba(0,0,0,0.08)', + borderStrong: 'rgba(0,0,0,0.14)', + ctaBg: '#1A1A18', + ctaText: '#F5F5F0', + serif: SERIF, + sans: SANS, + radius: 16, +}; + +export function useOnboardingSkin(): OnboardingSkin { + return useThemeMode().mode === 'dark' ? DARK : LIGHT; +} + +// framer-motion cubic-bezier for entrances. export const ONBOARDING_EASE = [0.16, 1, 0.3, 1] as const; diff --git a/frontend/src/app/components/Onboarding/flow/steps/ConnectApps.tsx b/frontend/src/app/components/Onboarding/flow/steps/ConnectApps.tsx index 011c1502..6b270b98 100644 --- a/frontend/src/app/components/Onboarding/flow/steps/ConnectApps.tsx +++ b/frontend/src/app/components/Onboarding/flow/steps/ConnectApps.tsx @@ -2,7 +2,7 @@ // UI-only here: real OAuth (integrations.tsx / POST /{tool_id}/oauth/start) wires in a follow step. import React from 'react'; -import { ONBOARDING_SKIN as S } from '../onboardingSkin'; +import { useOnboardingSkin } from '../onboardingSkin'; import { LineIcon } from '../OnboardingIcons'; import { Heading, Sub, PrimaryButton, GhostLink } from '../OnboardingAtoms'; import type { ConnectorOption } from '../onboardingFlowTypes'; @@ -17,43 +17,46 @@ const Row: React.FC<{ option: ConnectorOption; connected: boolean; onConnect: () option, connected, onConnect, -}) => ( -
- - - -
-
{option.name}
-
{option.description}
-
- { + const S = useOnboardingSkin(); + return ( +
- {connected ? 'Connected' : 'Connect'} - -
-); + + + +
+
{option.name}
+
{option.description}
+
+ + {connected ? 'Connected' : 'Connect'} + +
+ ); +}; export const ConnectApps: React.FC<{ connectedIds: string[]; diff --git a/frontend/src/app/components/Onboarding/flow/steps/PersonalizeConsent.tsx b/frontend/src/app/components/Onboarding/flow/steps/PersonalizeConsent.tsx index b322d947..4fb19dde 100644 --- a/frontend/src/app/components/Onboarding/flow/steps/PersonalizeConsent.tsx +++ b/frontend/src/app/components/Onboarding/flow/steps/PersonalizeConsent.tsx @@ -1,60 +1,53 @@ -// D3: the personalize consent. One serif line types itself in, then a single Yes. "Yes" authorizes -// the (later) background profiling read; copy states that plainly. This is a permission, not a checkbox. +// D3: the personalize consent. The line reveals word-by-word (smooth, layout-stable, no jumpy +// caret), then a single Yes. "Yes" authorizes the (later) background profiling read; copy says so. -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useReducedMotion } from '@/shared/hooks/useReducedMotion'; -import { ONBOARDING_SKIN as S } from '../onboardingSkin'; +import { useOnboardingSkin } from '../onboardingSkin'; import { PrimaryButton, GhostLink } from '../OnboardingAtoms'; const LINE = "Want me to actually get you? Say yes and I'll take a quick look at whatever you connect, so everything I show is aimed at your world, not a generic demo."; +const WORD_STAGGER_S = 0.055; +const WORD_DUR_S = 0.5; + export const PersonalizeConsent: React.FC<{ onConsent: (yes: boolean) => void }> = ({ onConsent }) => { const reduce = useReducedMotion(); - const [shown, setShown] = useState(reduce ? LINE.length : 0); - const done = shown >= LINE.length; + const S = useOnboardingSkin(); + const words = useMemo(() => LINE.split(' '), []); + const [done, setDone] = useState(reduce); useEffect(() => { - if (reduce) { setShown(LINE.length); return; } - setShown(0); - let i = 0; - const id = window.setInterval(() => { - i += 1; - setShown(i); - if (i >= LINE.length) window.clearInterval(id); - }, 16); - return () => window.clearInterval(id); - }, [reduce]); + if (reduce) { setDone(true); return; } + setDone(false); + const totalMs = (words.length * WORD_STAGGER_S + WORD_DUR_S) * 1000; + const t = window.setTimeout(() => setDone(true), totalMs); + return () => window.clearTimeout(t); + }, [reduce, words.length]); return ( <> -
- {LINE.slice(0, shown)} - +
+ {words.map((w, i) => ( + + + {w} + + {i < words.length - 1 ? ' ' : ''} + + ))}
void }> onConsent(true)}>Yes, get to know me onConsent(false)}>not now
- + ); }; diff --git a/frontend/src/app/components/Onboarding/flow/steps/WhatShouldICallYou.tsx b/frontend/src/app/components/Onboarding/flow/steps/WhatShouldICallYou.tsx index 03f8676f..874f373a 100644 --- a/frontend/src/app/components/Onboarding/flow/steps/WhatShouldICallYou.tsx +++ b/frontend/src/app/components/Onboarding/flow/steps/WhatShouldICallYou.tsx @@ -1,7 +1,7 @@ // D2: one field, the name. Claude single-control screen. Enter or Continue advances. import React, { useState } from 'react'; -import { ONBOARDING_SKIN as S } from '../onboardingSkin'; +import { useOnboardingSkin } from '../onboardingSkin'; import { Heading, Sub, PrimaryButton, GhostLink } from '../OnboardingAtoms'; export const WhatShouldICallYou: React.FC<{ @@ -9,6 +9,7 @@ export const WhatShouldICallYou: React.FC<{ onContinue: (name: string) => void; onSkip: () => void; }> = ({ initialName = '', onContinue, onSkip }) => { + const S = useOnboardingSkin(); const [name, setName] = useState(initialName); const [focus, setFocus] = useState(false); const submit = () => onContinue(name.trim()); diff --git a/frontend/src/app/components/Onboarding/flow/steps/WhereDoYouWantHelp.tsx b/frontend/src/app/components/Onboarding/flow/steps/WhereDoYouWantHelp.tsx index 6086f5c3..6f087b2f 100644 --- a/frontend/src/app/components/Onboarding/flow/steps/WhereDoYouWantHelp.tsx +++ b/frontend/src/app/components/Onboarding/flow/steps/WhereDoYouWantHelp.tsx @@ -1,7 +1,7 @@ // D1: one tap on a persona card. The single personalization seed. "just show me" skips to payoff. import React, { useState } from 'react'; -import { ONBOARDING_SKIN as S } from '../onboardingSkin'; +import { useOnboardingSkin } from '../onboardingSkin'; import { LineIcon } from '../OnboardingIcons'; import { Heading, GhostLink } from '../OnboardingAtoms'; import type { PersonaOption } from '../onboardingFlowTypes'; @@ -13,6 +13,7 @@ const PERSONAS: PersonaOption[] = [ ]; const Card: React.FC<{ option: PersonaOption; onPick: () => void }> = ({ option, onPick }) => { + const S = useOnboardingSkin(); const [hover, setHover] = useState(false); return (