From 54bc00ce3f1a01a8ffb5e0be5b0a37be4acdb32f Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:09:42 +0800 Subject: [PATCH] feat(desktop): redesign privacy step with single column layout - Simplify permissions step to single centered column - Replace per-item switches with single "I understand" checkbox - Add capabilities info card with clean dividers - Add trust note section for privacy messaging - Rename step from "Permissions" to "Privacy" for clarity - Add fade-in animation for step transitions - Add shadcn checkbox component Co-Authored-By: Claude Opus 4.5 --- apps/desktop/src/main/index.ts | 2 +- apps/desktop/src/renderer/src/App.tsx | 7 +- .../renderer/src/components/mode-toggle.tsx | 46 +++++++ .../src/components/onboarding/stepper.tsx | 2 +- .../src/components/theme-provider.tsx | 68 ++++++++++ .../onboarding/components/connect-step.tsx | 20 +-- .../components/permissions-step.tsx | 128 +++++++++--------- .../onboarding/components/setup-step.tsx | 4 +- .../pages/onboarding/components/step-dots.tsx | 30 ++++ .../onboarding/components/try-it-step.tsx | 4 +- .../renderer/src/pages/onboarding/index.tsx | 95 ++++++++----- packages/ui/src/components/ui/checkbox.tsx | 29 ++++ 12 files changed, 329 insertions(+), 106 deletions(-) create mode 100644 apps/desktop/src/renderer/src/components/mode-toggle.tsx create mode 100644 apps/desktop/src/renderer/src/components/theme-provider.tsx create mode 100644 apps/desktop/src/renderer/src/pages/onboarding/components/step-dots.tsx create mode 100644 packages/ui/src/components/ui/checkbox.tsx diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index df23ba44..46170ef4 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -74,7 +74,7 @@ function createWindow() { width: 1200, height: 800, titleBarStyle: 'hiddenInset', - trafficLightPosition: { x: 16, y: 12 }, + trafficLightPosition: { x: 16, y: 17 }, // Vertically centered in 48px header webPreferences: { preload: path.join(__dirname, '../preload/index.cjs'), // Enable node integration for IPC diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx index a1c70e85..2e83ab90 100644 --- a/apps/desktop/src/renderer/src/App.tsx +++ b/apps/desktop/src/renderer/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { createHashRouter, Navigate, RouterProvider } from 'react-router-dom' +import { ThemeProvider } from './components/theme-provider' import Layout from './pages/layout' import HomePage from './pages/home' import ChatPage from './pages/chat' @@ -48,5 +49,9 @@ export default function App() { useOnboardingStore.getState().initForceFlag() }, []) - return + return ( + + + + ) } diff --git a/apps/desktop/src/renderer/src/components/mode-toggle.tsx b/apps/desktop/src/renderer/src/components/mode-toggle.tsx new file mode 100644 index 00000000..19988e05 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/mode-toggle.tsx @@ -0,0 +1,46 @@ +import { HugeiconsIcon } from "@hugeicons/react" +import { Sun03Icon, Moon02Icon, ComputerIcon } from "@hugeicons/core-free-icons" +import { Button } from "@multica/ui/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@multica/ui/components/ui/dropdown-menu" +import { useTheme } from "./theme-provider" + +export function ModeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme("light")}> + + Light + + setTheme("dark")}> + + Dark + + setTheme("system")}> + + System + + + + ) +} diff --git a/apps/desktop/src/renderer/src/components/onboarding/stepper.tsx b/apps/desktop/src/renderer/src/components/onboarding/stepper.tsx index c860d244..1b154054 100644 --- a/apps/desktop/src/renderer/src/components/onboarding/stepper.tsx +++ b/apps/desktop/src/renderer/src/components/onboarding/stepper.tsx @@ -3,7 +3,7 @@ import { HugeiconsIcon } from '@hugeicons/react' import { Tick02Icon } from '@hugeicons/core-free-icons' const steps = [ - { label: 'Permissions' }, + { label: 'Privacy' }, { label: 'Provider' }, { label: 'Connect' }, { label: 'Try it' }, diff --git a/apps/desktop/src/renderer/src/components/theme-provider.tsx b/apps/desktop/src/renderer/src/components/theme-provider.tsx new file mode 100644 index 00000000..abf9600c --- /dev/null +++ b/apps/desktop/src/renderer/src/components/theme-provider.tsx @@ -0,0 +1,68 @@ +import { createContext, useContext, useEffect, useState } from "react" + +type Theme = "dark" | "light" | "system" + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null, +} + +const ThemeProviderContext = createContext(initialState) + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "multica-theme", +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ) + + useEffect(() => { + const root = window.document.documentElement + root.classList.remove("light", "dark") + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light" + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [theme]) + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme) + setTheme(theme) + }, + } + + return ( + + {children} + + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider") + return context +} diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx index 2693b4b6..1d553ee4 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx @@ -6,6 +6,7 @@ import { HugeiconsIcon } from '@hugeicons/react' import { ArrowLeft02Icon, Loading03Icon } from '@hugeicons/core-free-icons' import { useChannels } from '../../../hooks/use-channels' import { TutorialStep } from '../../../components/onboarding/tutorial-step' +import { StepDots } from './step-dots' import { useOnboardingStore } from '../../../stores/onboarding' function statusVariant( @@ -137,15 +138,18 @@ export default function ConnectStep({ onNext, onBack }: ConnectStepProps) { )} -
- {!hasToken && ( - + )} + - )} - +
diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/permissions-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/permissions-step.tsx index b013886b..c17ee9dd 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/components/permissions-step.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/permissions-step.tsx @@ -1,42 +1,36 @@ +import { useState } from 'react' import { Button } from '@multica/ui/components/ui/button' +import { Checkbox } from '@multica/ui/components/ui/checkbox' +import { Separator } from '@multica/ui/components/ui/separator' +import { HugeiconsIcon } from '@hugeicons/react' import { FolderOpenIcon, CommandLineIcon, AiBrainIcon, Database01Icon, } from '@hugeicons/core-free-icons' -import { AcknowledgementItem } from '../../../components/onboarding/permission-item' -import { PrivacyPanel } from '../../../components/onboarding/privacy-panel' -import { useOnboardingStore } from '../../../stores/onboarding' +import { StepDots } from './step-dots' -const acknowledgementItems = [ +const capabilities = [ { - key: 'fileSystem' as const, icon: FolderOpenIcon, - title: 'File system access', - description: - 'Multica reads and writes files on your machine to complete tasks you assign.', + title: 'File access', + description: 'Read & write files to complete tasks you assign', }, { - key: 'shellExecution' as const, icon: CommandLineIcon, - title: 'Shell command execution', - description: - 'The agent may run shell commands. Every command requires your explicit approval.', + title: 'Shell commands', + description: 'Run commands — every one requires your approval', }, { - key: 'llmRequests' as const, icon: AiBrainIcon, - title: 'LLM API requests', - description: - 'Multica sends prompts to your configured LLM provider. Your API key is used directly.', + title: 'LLM requests', + description: 'Send prompts using your API key directly', }, { - key: 'localStorage' as const, icon: Database01Icon, - title: 'Local data storage', - description: - 'Sessions, profiles, and credentials are stored locally in ~/.super-multica/', + title: 'Local storage', + description: 'Sessions & credentials saved in ~/.super-multica/', }, ] @@ -45,57 +39,67 @@ interface PermissionsStepProps { } export default function PermissionsStep({ onNext }: PermissionsStepProps) { - const { acknowledgements, allAcknowledged, setAcknowledgement } = - useOnboardingStore() + const [agreed, setAgreed] = useState(false) return ( -
- {/* Left column — main content, centered both axes */} -
-
-
-

- Privacy & trust -

-

- Multica works locally on your machine. Review what it accesses - and toggle each item to acknowledge. -

-
+
+
+ {/* Header */} +
+

+ Privacy & trust +

+

+ Multica works locally on your machine. Here's what it can do. +

+
-
- {acknowledgementItems.map((item) => ( - - setAcknowledgement(item.key, checked) - } + {/* Capabilities card */} +
+ {capabilities.map((item) => ( +
+
+ +
+
+

{item.title}

+

+ {item.description} +

+
+
+ ))} +
+ + {/* Trust note */} +
+

+ Everything stays on your machine. We have no servers and can't see your data. +

+
+ + + + {/* Footer: dots on left, checkbox + button on right */} +
+ +
+
- -
-
- - {/* Right column — privacy panel */} -
-
- -
-
) } diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/setup-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/setup-step.tsx index a2c8acbf..f5f9fb04 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/components/setup-step.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/setup-step.tsx @@ -7,6 +7,7 @@ import { ApiKeyDialog } from '../../../components/api-key-dialog' import { OAuthDialog } from '../../../components/oauth-dialog' import { ProviderSetup } from '../../../components/onboarding/provider-setup' import { TutorialStep } from '../../../components/onboarding/tutorial-step' +import { StepDots } from './step-dots' import { useOnboardingStore } from '../../../stores/onboarding' interface SetupStepProps { @@ -86,7 +87,8 @@ export default function SetupStep({ onNext, onBack }: SetupStepProps) {

{error}

)} -
+
+ diff --git a/apps/desktop/src/renderer/src/pages/onboarding/index.tsx b/apps/desktop/src/renderer/src/pages/onboarding/index.tsx index 2a9612f7..b0c7ff4e 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/index.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/index.tsx @@ -1,56 +1,89 @@ -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' +import { useNavigate } from "react-router-dom"; +import { useOnboardingStore } from "../../stores/onboarding"; +import { MulticaIcon } from "@multica/ui/components/multica-icon"; +import { ModeToggle } from "../../components/mode-toggle"; +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"; + +const steps = ["Privacy", "Provider", "Connect", "Try it"]; export default function OnboardingPage() { - const navigate = useNavigate() - const { currentStep, nextStep, prevStep, completeOnboarding } = useOnboardingStore() + const navigate = useNavigate(); + const { currentStep, nextStep, prevStep, completeOnboarding } = + useOnboardingStore(); const handleComplete = () => { - completeOnboarding() - navigate('/') - } + completeOnboarding(); + navigate("/"); + }; - // Welcome step (step 0) has no stepper + // Welcome step (step 0) has no header content, just draggable area if (currentStep === 0) { return (
- {/* Draggable title bar region for macOS */} + {/* Draggable title bar region for macOS - same height as main header */}
-
+
- ) + ); } + const stepLabel = steps[currentStep - 1]; + const totalSteps = steps.length; + return (
- {/* Draggable title bar region for macOS + stepper */} -
- {/* Spacer for traffic lights */} -
- +
+ {/* Left: Draggable area for traffic lights */} +
+ + {/* Brand */} +
+ + Multica +
+ + {/* Center: Step indicator */} +
+ + {stepLabel} ({currentStep}/{totalSteps}) + +
+ + {/* Right: Theme toggle */} +
+ +
{/* Step content */} -
+
{currentStep === 1 && } {currentStep === 2 && } - {currentStep === 3 && } - {currentStep === 4 && } + {currentStep === 3 && ( + + )} + {currentStep === 4 && ( + + )}
- ) + ); } diff --git a/packages/ui/src/components/ui/checkbox.tsx b/packages/ui/src/components/ui/checkbox.tsx new file mode 100644 index 00000000..77c41463 --- /dev/null +++ b/packages/ui/src/components/ui/checkbox.tsx @@ -0,0 +1,29 @@ +"use client" + +import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox" + +import { cn } from "@multica/ui/lib/utils" +import { HugeiconsIcon } from "@hugeicons/react" +import { Tick02Icon } from "@hugeicons/core-free-icons" + +function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { + return ( + + + + + + ) +} + +export { Checkbox }