diff --git a/packages/ui/package.json b/packages/ui/package.json index cec8f55c5a..324aa42715 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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", diff --git a/packages/ui/src/components/primitives/checkbox.tsx b/packages/ui/src/components/primitives/checkbox.tsx new file mode 100644 index 0000000000..c3817d5610 --- /dev/null +++ b/packages/ui/src/components/primitives/checkbox.tsx @@ -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 & VariantProps) { + return ( + + + + + + ) +} + +export { Checkbox, checkboxVariants } diff --git a/packages/ui/stories/components/primitives/Checkbox.stories.tsx b/packages/ui/stories/components/primitives/Checkbox.stories.tsx new file mode 100644 index 0000000000..be48f82af6 --- /dev/null +++ b/packages/ui/stories/components/primitives/Checkbox.stories.tsx @@ -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 = { + 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 + +// Default +export const Default: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ) +} + +// With Default Checked +export const WithDefaultChecked: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ) +} + +// Disabled +export const Disabled: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ ) +} + +// Controlled +export const Controlled: Story = { + render: function ControlledExample() { + const [checked, setChecked] = useState(false) + + return ( +
+
+ + +
+
Current state: {checked ? 'Checked' : 'Unchecked'}
+
+ ) + } +} + +// Sizes +export const Sizes: Story = { + render: () => ( +
+
+

Small (sm)

+
+
+ + +
+
+ + +
+
+
+ +
+

Medium (md) - Default

+
+
+ + +
+
+ + +
+
+
+ +
+

Large (lg)

+
+
+ + +
+
+ + +
+
+
+
+ ) +} + +// All States +export const AllStates: Story = { + render: function AllStatesExample() { + const [normalChecked, setNormalChecked] = useState(false) + const [checkedState, setCheckedState] = useState(true) + + return ( +
+ {/* Normal State (Unchecked) */} +
+

Normal State (Unchecked)

+
+ + +
+
+ + {/* Checked State */} +
+

Checked State

+
+ + +
+
+ + {/* Disabled State (Unchecked) */} +
+

Disabled State (Unchecked)

+
+ + +
+
+ + {/* Disabled State (Checked) */} +
+

Disabled State (Checked)

+
+ + +
+
+ + {/* Error State */} +
+

Error State

+
+ + +
+

This field is required

+
+
+ ) + } +} + +// 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 ( +
+ {/* Notification Settings */} +
+

Notification Preferences

+
+
+ setSettings({ ...settings, emailNotifications: !!checked })} + /> + +
+
+ setSettings({ ...settings, pushNotifications: !!checked })} + /> + +
+
+ setSettings({ ...settings, smsNotifications: !!checked })} + /> + +
+
+ setSettings({ ...settings, newsletter: !!checked })} + /> + +
+
+
+ + {/* Feature Toggles */} +
+

Feature Toggles

+
+
+ setFeatures({ ...features, analytics: !!checked })} + className="mt-1" + /> + +
+
+ setFeatures({ ...features, backup: !!checked })} + className="mt-1" + /> + +
+
+ setFeatures({ ...features, security: !!checked })} + className="mt-1" + /> + +
+
+ setFeatures({ ...features, api: !!checked })} + className="mt-1" + /> + +
+
+
+ + {/* Required Agreement */} +
+

+ Terms and Conditions * +

+
+ + +
+

You must accept the terms and conditions to continue

+
+
+ ) + } +} + +// Size Comparison +export const SizeComparison: Story = { + render: () => ( +
+
+

Unchecked

+
+
+ + sm +
+
+ + md +
+
+ + lg +
+
+
+ +
+

Checked

+
+
+ + sm +
+
+ + md +
+
+ + lg +
+
+
+
+ ) +} + +// 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 ( +
+

Account Registration

+ +
+
+ setFormData({ ...formData, terms: !!checked })} + aria-invalid={submitted && !formData.terms} + className="mt-0.5" + /> + +
+ {submitted && !formData.terms &&

This field is required

} + +
+ setFormData({ ...formData, privacy: !!checked })} + aria-invalid={submitted && !formData.privacy} + className="mt-0.5" + /> + +
+ {submitted && !formData.privacy &&

This field is required

} + +
+ setFormData({ ...formData, marketing: !!checked })} + className="mt-0.5" + /> + +
+
+ + + + {submitted && (formData.terms && formData.privacy) && ( +

Registration successful!

+ )} +
+ ) + } +} diff --git a/yarn.lock b/yarn.lock index 724cc60982..0d9695eca5 100644 --- a/yarn.lock +++ b/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"