-
diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx
new file mode 100644
index 00000000..824333c1
--- /dev/null
+++ b/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx
@@ -0,0 +1,65 @@
+import { Button } from '@multica/ui/components/ui/button'
+import { MulticaIcon } from '@multica/ui/components/multica-icon'
+
+const features = [
+ {
+ title: 'Your AI',
+ description: 'Choose your preferred model. Extend its abilities with Skills.',
+ },
+ {
+ title: 'Your Machine',
+ description: 'Runs locally on your computer. Your data stays with you.',
+ },
+ {
+ title: 'Your Control',
+ description: 'You set the boundaries. The AI works within them.',
+ },
+]
+
+interface WelcomeStepProps {
+ onStart: () => void
+}
+
+export default function WelcomeStep({ onStart }: WelcomeStepProps) {
+ return (
+
+
+ {/* Brand Title */}
+
+
+
+ Welcome to Multica
+
+
+
+ {/* Intro */}
+
+ An AI assistant that gets things done — pulling data, running analysis,
+ and taking action. Talk to it like a team member.
+
+
+ {/* Feature List */}
+
+
+ Built on three principles
+
+ {features.map((feature) => (
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+ ))}
+
+
+ {/* CTA Button */}
+
+ Start Exploring
+
+
+
+ )
+}
diff --git a/apps/desktop/src/renderer/src/pages/onboarding/index.tsx b/apps/desktop/src/renderer/src/pages/onboarding/index.tsx
new file mode 100644
index 00000000..2a9612f7
--- /dev/null
+++ b/apps/desktop/src/renderer/src/pages/onboarding/index.tsx
@@ -0,0 +1,56 @@
+import { useNavigate } from 'react-router-dom'
+import { Stepper } from '../../components/onboarding/stepper'
+import { useOnboardingStore } from '../../stores/onboarding'
+import WelcomeStep from './components/welcome-step'
+import PermissionsStep from './components/permissions-step'
+import SetupStep from './components/setup-step'
+import ConnectStep from './components/connect-step'
+import TryItStep from './components/try-it-step'
+
+export default function OnboardingPage() {
+ const navigate = useNavigate()
+ const { currentStep, nextStep, prevStep, completeOnboarding } = useOnboardingStore()
+
+ const handleComplete = () => {
+ completeOnboarding()
+ navigate('/')
+ }
+
+ // Welcome step (step 0) has no stepper
+ if (currentStep === 0) {
+ return (
+
+ {/* Draggable title bar region for macOS */}
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Draggable title bar region for macOS + stepper */}
+
+ {/* Spacer for traffic lights */}
+
+
+
+
+ {/* Step content */}
+
+ {currentStep === 1 && }
+ {currentStep === 2 && }
+ {currentStep === 3 && }
+ {currentStep === 4 && }
+
+
+ )
+}
diff --git a/apps/desktop/src/renderer/src/pages/onboarding/layout.tsx b/apps/desktop/src/renderer/src/pages/onboarding/layout.tsx
deleted file mode 100644
index eb04d8fa..00000000
--- a/apps/desktop/src/renderer/src/pages/onboarding/layout.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Outlet, useLocation } from 'react-router-dom'
-import { Stepper, type StepId } from '../../components/onboarding/stepper'
-
-export default function OnboardingLayout() {
- const location = useLocation()
-
- // Derive current step from URL path
- const pathSegment = location.pathname.split('/').pop() as string
- const validSteps: StepId[] = ['permissions', 'setup', 'connect', 'try-it']
- const currentStep: StepId = validSteps.includes(pathSegment as StepId)
- ? (pathSegment as StepId)
- : 'permissions'
-
- return (
-
- {/* Draggable title bar region for macOS + stepper */}
-
- {/* Spacer for traffic lights */}
-
-
-
-
- {/* Step content */}
-
-
-
-
- )
-}
diff --git a/apps/desktop/src/renderer/src/stores/onboarding.ts b/apps/desktop/src/renderer/src/stores/onboarding.ts
index 3282025a..d627e7fc 100644
--- a/apps/desktop/src/renderer/src/stores/onboarding.ts
+++ b/apps/desktop/src/renderer/src/stores/onboarding.ts
@@ -11,10 +11,14 @@ interface AcknowledgementsState {
interface OnboardingStore {
completed: boolean
forceOnboarding: boolean
+ currentStep: number
acknowledgements: AcknowledgementsState
allAcknowledged: boolean
providerConfigured: boolean
clientConnected: boolean
+ setStep: (step: number) => void
+ nextStep: () => void
+ prevStep: () => void
setAcknowledgement: (key: keyof AcknowledgementsState, value: boolean) => void
setProviderConfigured: (configured: boolean) => void
setClientConnected: (connected: boolean) => void
@@ -27,6 +31,7 @@ export const useOnboardingStore = create
()(
(set, get) => ({
completed: false,
forceOnboarding: false,
+ currentStep: 0,
acknowledgements: {
fileSystem: false,
@@ -38,6 +43,10 @@ export const useOnboardingStore = create()(
providerConfigured: false,
clientConnected: false,
+ setStep: (step) => set({ currentStep: step }),
+ nextStep: () => set({ currentStep: Math.min(get().currentStep + 1, 4) }),
+ prevStep: () => set({ currentStep: Math.max(get().currentStep - 1, 0) }),
+
setAcknowledgement: (key, value) => {
const acknowledgements = { ...get().acknowledgements, [key]: value }
const allAcknowledged = Object.values(acknowledgements).every(Boolean)
@@ -48,7 +57,7 @@ export const useOnboardingStore = create()(
setClientConnected: (connected) => set({ clientConnected: connected }),
- completeOnboarding: () => set({ completed: true, forceOnboarding: false }),
+ completeOnboarding: () => set({ completed: true, forceOnboarding: false, currentStep: 0 }),
initForceFlag: async () => {
const flags = await window.electronAPI.app.getFlags()
diff --git a/packages/ui/src/components/multica-icon.tsx b/packages/ui/src/components/multica-icon.tsx
index cfb222bd..c5d87e30 100644
--- a/packages/ui/src/components/multica-icon.tsx
+++ b/packages/ui/src/components/multica-icon.tsx
@@ -1,5 +1,12 @@
import { cn } from "@multica/ui/lib/utils";
+interface MulticaIconProps extends React.ComponentProps<"span"> {
+ /**
+ * If true, play a one-time entrance spin animation (2 seconds).
+ */
+ animate?: boolean;
+}
+
/**
* Pure CSS 8-pointed asterisk icon matching the Multica logo.
* Uses currentColor so it adapts to light/dark themes automatically.
@@ -7,11 +14,16 @@ import { cn } from "@multica/ui/lib/utils";
*/
export function MulticaIcon({
className,
+ animate = false,
...props
-}: React.ComponentProps<"span">) {
+}: MulticaIconProps) {
return (
diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css
index c7ea571b..aa693af3 100644
--- a/packages/ui/src/styles/globals.css
+++ b/packages/ui/src/styles/globals.css
@@ -189,3 +189,14 @@
0%, 100% { box-shadow: 0 0 0 0 var(--tool-running); }
50% { box-shadow: 0 0 0 3px oklch(0.6 0.2 250 / 0); }
}
+
+/* Welcome page: one-time spin animation for icon */
+@keyframes welcome-spin {
+ 0% { transform: rotate(0deg); opacity: 0; }
+ 30% { opacity: 1; }
+ 100% { transform: rotate(360deg); opacity: 1; }
+}
+
+.animate-welcome-spin {
+ animation: welcome-spin 2s ease-out forwards;
+}