mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
feat(checkbox): add Radix UI checkbox component with Storybook examples
- Introduced a new checkbox component utilizing Radix UI, allowing for customizable sizes and states. - Implemented styles using class-variance-authority for consistent design across different sizes (sm, md, lg). - Added comprehensive Storybook stories demonstrating various use cases, including default, checked, disabled, and controlled states. - Updated package.json and yarn.lock to include the new Radix UI checkbox dependency.
This commit is contained in:
parent
75c0923636
commit
02d79f47b3
@ -48,6 +48,7 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
|
||||
63
packages/ui/src/components/primitives/checkbox.tsx
Normal file
63
packages/ui/src/components/primitives/checkbox.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { cn } from '@cherrystudio/ui/utils/index'
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { CheckIcon } from 'lucide-react'
|
||||
import * as React from 'react'
|
||||
|
||||
const checkboxVariants = cva(
|
||||
cn(
|
||||
'aspect-square shrink-0 rounded-[4px] border transition-all outline-none',
|
||||
'border-primary text-primary',
|
||||
'hover:bg-primary/10',
|
||||
'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary',
|
||||
'focus-visible:ring-3 focus-visible:ring-primary/20',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
'disabled:cursor-not-allowed disabled:border-gray-500/10 disabled:bg-background-subtle',
|
||||
'bg-white/10 shadow-xs'
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'size-4',
|
||||
md: 'size-5',
|
||||
lg: 'size-6'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const checkboxIconVariants = cva('dark:text-white', {
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'size-3',
|
||||
md: 'size-3.5',
|
||||
lg: 'size-4'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md'
|
||||
}
|
||||
})
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
size = 'md',
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root> & VariantProps<typeof checkboxVariants>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
data-size={size}
|
||||
className={cn(checkboxVariants({ size }), className)}
|
||||
{...props}>
|
||||
<CheckboxPrimitive.Indicator data-slot="checkbox-indicator" className="grid place-content-center transition-none">
|
||||
<CheckIcon className={checkboxIconVariants({ size })} />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox, checkboxVariants }
|
||||
533
packages/ui/stories/components/primitives/Checkbox.stories.tsx
Normal file
533
packages/ui/stories/components/primitives/Checkbox.stories.tsx
Normal file
@ -0,0 +1,533 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { Bell, Check, FileText, Mail, Shield, Star } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Checkbox } from '../../../src/components/primitives/checkbox'
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: 'Components/Primitives/Checkbox',
|
||||
component: Checkbox,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'A checkbox component based on Radix UI, allowing users to select multiple options. Supports three sizes (sm, md, lg) as defined in the Figma design system.'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
disabled: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Whether the checkbox is disabled'
|
||||
},
|
||||
defaultChecked: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Default checked state'
|
||||
},
|
||||
checked: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Checked state in controlled mode'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Default
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="default1" />
|
||||
<label htmlFor="default1" className="cursor-pointer text-sm">
|
||||
Accept terms and conditions
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="default2" />
|
||||
<label htmlFor="default2" className="cursor-pointer text-sm">
|
||||
Subscribe to newsletter
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="default3" />
|
||||
<label htmlFor="default3" className="cursor-pointer text-sm">
|
||||
Enable notifications
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Default Checked
|
||||
export const WithDefaultChecked: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="checked1" defaultChecked />
|
||||
<label htmlFor="checked1" className="cursor-pointer text-sm">
|
||||
Option 1 (Default Checked)
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="checked2" />
|
||||
<label htmlFor="checked2" className="cursor-pointer text-sm">
|
||||
Option 2
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="checked3" defaultChecked />
|
||||
<label htmlFor="checked3" className="cursor-pointer text-sm">
|
||||
Option 3 (Default Checked)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Disabled
|
||||
export const Disabled: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="disabled1" disabled />
|
||||
<label htmlFor="disabled1" className="cursor-not-allowed text-sm opacity-50">
|
||||
Disabled (Unchecked)
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="disabled2" disabled defaultChecked />
|
||||
<label htmlFor="disabled2" className="cursor-not-allowed text-sm opacity-50">
|
||||
Disabled (Checked)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Controlled
|
||||
export const Controlled: Story = {
|
||||
render: function ControlledExample() {
|
||||
const [checked, setChecked] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="controlled" checked={checked} onCheckedChange={setChecked} />
|
||||
<label htmlFor="controlled" className="cursor-pointer text-sm">
|
||||
Controlled checkbox
|
||||
</label>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Current state: {checked ? 'Checked' : 'Unchecked'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Sizes
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<p className="mb-3 text-sm text-muted-foreground">Small (sm)</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="size-sm-1" size="sm" />
|
||||
<label htmlFor="size-sm-1" className="cursor-pointer text-sm">
|
||||
Small checkbox
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="size-sm-2" size="sm" defaultChecked />
|
||||
<label htmlFor="size-sm-2" className="cursor-pointer text-sm">
|
||||
Small checkbox (checked)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-3 text-sm text-muted-foreground">Medium (md) - Default</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="size-md-1" size="md" />
|
||||
<label htmlFor="size-md-1" className="cursor-pointer text-sm">
|
||||
Medium checkbox
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="size-md-2" size="md" defaultChecked />
|
||||
<label htmlFor="size-md-2" className="cursor-pointer text-sm">
|
||||
Medium checkbox (checked)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-3 text-sm text-muted-foreground">Large (lg)</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="size-lg-1" size="lg" />
|
||||
<label htmlFor="size-lg-1" className="cursor-pointer text-sm">
|
||||
Large checkbox
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="size-lg-2" size="lg" defaultChecked />
|
||||
<label htmlFor="size-lg-2" className="cursor-pointer text-sm">
|
||||
Large checkbox (checked)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// All States
|
||||
export const AllStates: Story = {
|
||||
render: function AllStatesExample() {
|
||||
const [normalChecked, setNormalChecked] = useState(false)
|
||||
const [checkedState, setCheckedState] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Normal State (Unchecked) */}
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-muted-foreground">Normal State (Unchecked)</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="state-normal" checked={normalChecked} onCheckedChange={setNormalChecked} />
|
||||
<label htmlFor="state-normal" className="cursor-pointer text-sm">
|
||||
Unchecked Option
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checked State */}
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-muted-foreground">Checked State</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="state-checked" checked={checkedState} onCheckedChange={setCheckedState} />
|
||||
<label htmlFor="state-checked" className="cursor-pointer text-sm">
|
||||
Checked Option
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disabled State (Unchecked) */}
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-muted-foreground">Disabled State (Unchecked)</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="state-disabled-unchecked" disabled />
|
||||
<label htmlFor="state-disabled-unchecked" className="cursor-not-allowed text-sm opacity-50">
|
||||
Disabled (Unchecked)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disabled State (Checked) */}
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-muted-foreground">Disabled State (Checked)</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="state-disabled-checked" disabled defaultChecked />
|
||||
<label htmlFor="state-disabled-checked" className="cursor-not-allowed text-sm opacity-50">
|
||||
Disabled (Checked)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error State */}
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-muted-foreground">Error State</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="state-error" aria-invalid />
|
||||
<label htmlFor="state-error" className="cursor-pointer text-sm">
|
||||
Required Field
|
||||
</label>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-destructive">This field is required</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Real World Examples
|
||||
export const RealWorldExamples: Story = {
|
||||
render: function RealWorldExample() {
|
||||
const [settings, setSettings] = useState({
|
||||
emailNotifications: true,
|
||||
pushNotifications: false,
|
||||
smsNotifications: false,
|
||||
newsletter: true
|
||||
})
|
||||
|
||||
const [features, setFeatures] = useState({
|
||||
analytics: true,
|
||||
backup: false,
|
||||
security: true,
|
||||
api: false
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* Notification Settings */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Notification Preferences</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id="notif-email"
|
||||
checked={settings.emailNotifications}
|
||||
onCheckedChange={(checked) => setSettings({ ...settings, emailNotifications: !!checked })}
|
||||
/>
|
||||
<label htmlFor="notif-email" className="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<Mail className="size-4" />
|
||||
Email Notifications
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id="notif-push"
|
||||
checked={settings.pushNotifications}
|
||||
onCheckedChange={(checked) => setSettings({ ...settings, pushNotifications: !!checked })}
|
||||
/>
|
||||
<label htmlFor="notif-push" className="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<Bell className="size-4" />
|
||||
Push Notifications
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id="notif-sms"
|
||||
checked={settings.smsNotifications}
|
||||
onCheckedChange={(checked) => setSettings({ ...settings, smsNotifications: !!checked })}
|
||||
/>
|
||||
<label htmlFor="notif-sms" className="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<FileText className="size-4" />
|
||||
SMS Notifications
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id="notif-newsletter"
|
||||
checked={settings.newsletter}
|
||||
onCheckedChange={(checked) => setSettings({ ...settings, newsletter: !!checked })}
|
||||
/>
|
||||
<label htmlFor="notif-newsletter" className="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<Star className="size-4" />
|
||||
Newsletter Subscription
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Toggles */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">Feature Toggles</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id="feature-analytics"
|
||||
checked={features.analytics}
|
||||
onCheckedChange={(checked) => setFeatures({ ...features, analytics: !!checked })}
|
||||
className="mt-1"
|
||||
/>
|
||||
<label htmlFor="feature-analytics" className="cursor-pointer">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Check className="size-4" />
|
||||
Analytics
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Track user behavior and app performance</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id="feature-backup"
|
||||
checked={features.backup}
|
||||
onCheckedChange={(checked) => setFeatures({ ...features, backup: !!checked })}
|
||||
className="mt-1"
|
||||
/>
|
||||
<label htmlFor="feature-backup" className="cursor-pointer">
|
||||
<div className="text-sm font-medium">Automatic Backup</div>
|
||||
<div className="text-xs text-muted-foreground">Backup data every 24 hours</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id="feature-security"
|
||||
checked={features.security}
|
||||
onCheckedChange={(checked) => setFeatures({ ...features, security: !!checked })}
|
||||
className="mt-1"
|
||||
/>
|
||||
<label htmlFor="feature-security" className="cursor-pointer">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Shield className="size-4" />
|
||||
Advanced Security
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Enable two-factor authentication</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id="feature-api"
|
||||
checked={features.api}
|
||||
onCheckedChange={(checked) => setFeatures({ ...features, api: !!checked })}
|
||||
className="mt-1"
|
||||
/>
|
||||
<label htmlFor="feature-api" className="cursor-pointer">
|
||||
<div className="text-sm font-medium">API Access</div>
|
||||
<div className="text-xs text-muted-foreground">Enable programmatic access</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Required Agreement */}
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold">
|
||||
Terms and Conditions <span className="text-destructive">*</span>
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox id="terms" aria-invalid />
|
||||
<label htmlFor="terms" className="cursor-pointer text-sm">
|
||||
I agree to the terms and conditions
|
||||
</label>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-destructive">You must accept the terms and conditions to continue</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Size Comparison
|
||||
export const SizeComparison: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-xs font-medium text-muted-foreground">Unchecked</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Checkbox id="compare-sm-1" size="sm" />
|
||||
<span className="text-xs text-muted-foreground">sm</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Checkbox id="compare-md-1" size="md" />
|
||||
<span className="text-xs text-muted-foreground">md</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Checkbox id="compare-lg-1" size="lg" />
|
||||
<span className="text-xs text-muted-foreground">lg</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-xs font-medium text-muted-foreground">Checked</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Checkbox id="compare-sm-2" size="sm" defaultChecked />
|
||||
<span className="text-xs text-muted-foreground">sm</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Checkbox id="compare-md-2" size="md" defaultChecked />
|
||||
<span className="text-xs text-muted-foreground">md</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Checkbox id="compare-lg-2" size="lg" defaultChecked />
|
||||
<span className="text-xs text-muted-foreground">lg</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Form Example
|
||||
export const FormExample: Story = {
|
||||
render: function FormExample() {
|
||||
const [formData, setFormData] = useState({
|
||||
terms: false,
|
||||
privacy: false,
|
||||
marketing: false
|
||||
})
|
||||
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSubmitted(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="w-80 space-y-4">
|
||||
<h3 className="text-sm font-semibold">Account Registration</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id="form-terms"
|
||||
checked={formData.terms}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, terms: !!checked })}
|
||||
aria-invalid={submitted && !formData.terms}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<label htmlFor="form-terms" className="cursor-pointer text-sm leading-relaxed">
|
||||
I agree to the{' '}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Terms of Service
|
||||
</a>{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</label>
|
||||
</div>
|
||||
{submitted && !formData.terms && <p className="text-xs text-destructive">This field is required</p>}
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id="form-privacy"
|
||||
checked={formData.privacy}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, privacy: !!checked })}
|
||||
aria-invalid={submitted && !formData.privacy}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<label htmlFor="form-privacy" className="cursor-pointer text-sm leading-relaxed">
|
||||
I acknowledge the{' '}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Privacy Policy
|
||||
</a>{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</label>
|
||||
</div>
|
||||
{submitted && !formData.privacy && <p className="text-xs text-destructive">This field is required</p>}
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id="form-marketing"
|
||||
checked={formData.marketing}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, marketing: !!checked })}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<label htmlFor="form-marketing" className="cursor-pointer text-sm leading-relaxed">
|
||||
I want to receive marketing emails and promotional offers
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
|
||||
Register
|
||||
</button>
|
||||
|
||||
{submitted && (formData.terms && formData.privacy) && (
|
||||
<p className="text-sm text-green-600">Registration successful!</p>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
27
yarn.lock
27
yarn.lock
@ -2028,6 +2028,7 @@ __metadata:
|
||||
"@dnd-kit/sortable": "npm:^10.0.0"
|
||||
"@dnd-kit/utilities": "npm:^3.2.2"
|
||||
"@heroui/react": "npm:^2.8.4"
|
||||
"@radix-ui/react-checkbox": "npm:^1.3.3"
|
||||
"@radix-ui/react-dialog": "npm:^1.1.15"
|
||||
"@radix-ui/react-popover": "npm:^1.1.15"
|
||||
"@radix-ui/react-radio-group": "npm:^1.3.8"
|
||||
@ -6955,6 +6956,32 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-checkbox@npm:^1.3.3":
|
||||
version: 1.3.3
|
||||
resolution: "@radix-ui/react-checkbox@npm:1.3.3"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": "npm:1.1.3"
|
||||
"@radix-ui/react-compose-refs": "npm:1.1.2"
|
||||
"@radix-ui/react-context": "npm:1.1.2"
|
||||
"@radix-ui/react-presence": "npm:1.1.5"
|
||||
"@radix-ui/react-primitive": "npm:2.1.3"
|
||||
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
|
||||
"@radix-ui/react-use-previous": "npm:1.1.1"
|
||||
"@radix-ui/react-use-size": "npm:1.1.1"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/5eeb78e37a6c9611a638a80b309c931dd6f1f8968357ab2abb453505392fa1397491441447ca2d5f4381faaac7fab2dc84c780e8ce27d931bd203fa014088b74
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-collection@npm:1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@radix-ui/react-collection@npm:1.1.7"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user