+
+
@@ -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 (