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 <noreply@anthropic.com>
This commit is contained in:
parent
7927903f32
commit
54bc00ce3f
12 changed files with 329 additions and 106 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <RouterProvider router={router} />
|
||||
return (
|
||||
<ThemeProvider defaultTheme="system" storageKey="multica-theme">
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
46
apps/desktop/src/renderer/src/components/mode-toggle.tsx
Normal file
46
apps/desktop/src/renderer/src/components/mode-toggle.tsx
Normal file
|
|
@ -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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-8">
|
||||
<HugeiconsIcon
|
||||
icon={Sun03Icon}
|
||||
className="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
||||
/>
|
||||
<HugeiconsIcon
|
||||
icon={Moon02Icon}
|
||||
className="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
||||
/>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
<HugeiconsIcon icon={Sun03Icon} className="size-4" />
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
<HugeiconsIcon icon={Moon02Icon} className="size-4" />
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
<HugeiconsIcon icon={ComputerIcon} className="size-4" />
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
|
@ -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' },
|
||||
|
|
|
|||
68
apps/desktop/src/renderer/src/components/theme-provider.tsx
Normal file
68
apps/desktop/src/renderer/src/components/theme-provider.tsx
Normal file
|
|
@ -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<ThemeProviderState>(initialState)
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "multica-theme",
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (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 (
|
||||
<ThemeProviderContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext)
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
return context
|
||||
}
|
||||
|
|
@ -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) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
{!hasToken && (
|
||||
<Button size="lg" variant="ghost" onClick={onNext}>
|
||||
Skip
|
||||
<div className="flex items-center justify-between">
|
||||
<StepDots />
|
||||
<div className="flex gap-2">
|
||||
{!hasToken && (
|
||||
<Button size="lg" variant="ghost" onClick={onNext}>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
<Button size="lg" onClick={onNext} disabled={!isRunning}>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
<Button size="lg" onClick={onNext} disabled={!isRunning}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="h-full flex">
|
||||
{/* Left column — main content, centered both axes */}
|
||||
<div className="flex-1 flex items-center justify-center px-12 py-8">
|
||||
<div className="max-w-md w-full space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Privacy & trust
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Multica works locally on your machine. Review what it accesses
|
||||
and toggle each item to acknowledge.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-full flex items-center justify-center px-6 py-8">
|
||||
<div className="w-full max-w-md space-y-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Privacy & trust
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Multica works locally on your machine. Here's what it can do.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{acknowledgementItems.map((item) => (
|
||||
<AcknowledgementItem
|
||||
key={item.key}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
checked={acknowledgements[item.key]}
|
||||
onCheckedChange={(checked) =>
|
||||
setAcknowledgement(item.key, checked)
|
||||
}
|
||||
{/* Capabilities card */}
|
||||
<div className="rounded-xl border border-border bg-card divide-y divide-border">
|
||||
{capabilities.map((item) => (
|
||||
<div key={item.title} className="flex items-start gap-3 p-4">
|
||||
<div className="mt-0.5 flex items-center justify-center size-8 rounded-lg bg-muted shrink-0">
|
||||
<HugeiconsIcon
|
||||
icon={item.icon}
|
||||
className="size-4 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">{item.title}</p>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Trust note */}
|
||||
<div className="rounded-lg bg-muted/50 px-4 py-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Everything stays on your machine. We have no servers and can't see your data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Footer: dots on left, checkbox + button on right */}
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<StepDots />
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={agreed}
|
||||
onCheckedChange={(checked) => setAgreed(checked === true)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={onNext}
|
||||
disabled={!allAcknowledged}
|
||||
>
|
||||
I understand
|
||||
</label>
|
||||
<Button size="sm" onClick={onNext} disabled={!agreed}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column — privacy panel */}
|
||||
<div className="flex-1 flex items-center justify-center bg-muted/30 px-12 py-8">
|
||||
<div className="max-w-sm">
|
||||
<PrivacyPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center justify-between">
|
||||
<StepDots />
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={onNext}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { cn } from "@multica/ui/lib/utils"
|
||||
import { useOnboardingStore } from "../../../stores/onboarding"
|
||||
|
||||
const TOTAL_STEPS = 4
|
||||
|
||||
export function StepDots() {
|
||||
const currentStep = useOnboardingStore((s) => s.currentStep)
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{Array.from({ length: TOTAL_STEPS }, (_, i) => {
|
||||
const step = i + 1 // steps are 1-based (1, 2, 3, 4)
|
||||
const isActive = step === currentStep
|
||||
const isCompleted = step < currentStep
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step}
|
||||
className={cn(
|
||||
"size-1.5 rounded-full transition-colors",
|
||||
isActive && "bg-foreground",
|
||||
isCompleted && "bg-foreground/50",
|
||||
!isActive && !isCompleted && "bg-muted-foreground/30"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { ChatView } from '@multica/ui/components/chat-view'
|
|||
import { HugeiconsIcon } from '@hugeicons/react'
|
||||
import { ArrowLeft02Icon } from '@hugeicons/core-free-icons'
|
||||
import { SamplePrompt } from '../../../components/onboarding/sample-prompt'
|
||||
import { StepDots } from './step-dots'
|
||||
import { useLocalChat } from '../../../hooks/use-local-chat'
|
||||
|
||||
const samplePrompts = [
|
||||
|
|
@ -80,7 +81,8 @@ export default function TryItStep({ onComplete, onBack }: TryItStepProps) {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center justify-between">
|
||||
<StepDots />
|
||||
<Button size="lg" onClick={onComplete}>
|
||||
Open Multica
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="h-dvh flex flex-col bg-background">
|
||||
{/* Draggable title bar region for macOS */}
|
||||
{/* Draggable title bar region for macOS - same height as main header */}
|
||||
<header
|
||||
className="shrink-0 h-8"
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
className="shrink-0 h-12"
|
||||
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
|
||||
/>
|
||||
<main className="flex-1 overflow-auto">
|
||||
<main
|
||||
key={currentStep}
|
||||
className="flex-1 overflow-auto animate-in fade-in duration-300"
|
||||
>
|
||||
<WelcomeStep onStart={nextStep} />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const stepLabel = steps[currentStep - 1];
|
||||
const totalSteps = steps.length;
|
||||
|
||||
return (
|
||||
<div className="h-dvh flex flex-col bg-background">
|
||||
{/* Draggable title bar region for macOS + stepper */}
|
||||
<header
|
||||
className="shrink-0 px-6 pt-3 pb-2"
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
{/* Spacer for traffic lights */}
|
||||
<div className="h-5" />
|
||||
<Stepper currentStep={currentStep} />
|
||||
<header className="shrink-0 h-12 flex items-center pr-4">
|
||||
{/* Left: Draggable area for traffic lights */}
|
||||
<div
|
||||
className="w-20 h-full shrink-0"
|
||||
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
|
||||
/>
|
||||
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<MulticaIcon className="size-4 text-muted-foreground/70" />
|
||||
<span className="text-sm tracking-wide font-brand">Multica</span>
|
||||
</div>
|
||||
|
||||
{/* Center: Step indicator */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{stepLabel} ({currentStep}/{totalSteps})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Right: Theme toggle */}
|
||||
<div className="shrink-0">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Step content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<main
|
||||
key={currentStep}
|
||||
className="flex-1 overflow-auto animate-in fade-in duration-300"
|
||||
>
|
||||
{currentStep === 1 && <PermissionsStep onNext={nextStep} />}
|
||||
{currentStep === 2 && <SetupStep onNext={nextStep} onBack={prevStep} />}
|
||||
{currentStep === 3 && <ConnectStep onNext={nextStep} onBack={prevStep} />}
|
||||
{currentStep === 4 && <TryItStep onComplete={handleComplete} onBack={prevStep} />}
|
||||
{currentStep === 3 && (
|
||||
<ConnectStep onNext={nextStep} onBack={prevStep} />
|
||||
)}
|
||||
{currentStep === 4 && (
|
||||
<TryItStep onComplete={handleComplete} onBack={prevStep} />
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
29
packages/ui/src/components/ui/checkbox.tsx
Normal file
29
packages/ui/src/components/ui/checkbox.tsx
Normal file
|
|
@ -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 (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
|
||||
>
|
||||
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
Loading…
Add table
Add a link
Reference in a new issue