mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 11:49:02 +08:00
feat(radioGroup): enhance RadioGroup component with size variants and add Storybook examples
- Introduced size variants for RadioGroupItem using class-variance-authority for better customization. - Updated RadioGroupItem to accept size prop and adjusted styles accordingly. - Added comprehensive Storybook stories for various use cases, including default, disabled, and size variations, to demonstrate component functionality and usage.
This commit is contained in:
parent
7dd1ecd4a5
commit
75c0923636
@ -1,28 +1,57 @@
|
|||||||
import { cn } from '@cherrystudio/ui/utils/index'
|
import { cn } from '@cherrystudio/ui/utils/index'
|
||||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
|
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import { CircleIcon } from 'lucide-react'
|
import { CircleIcon } from 'lucide-react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
|
const radioGroupItemVariants = cva(
|
||||||
|
cn(
|
||||||
|
'aspect-square shrink-0 rounded-full border transition-all outline-none',
|
||||||
|
'border-primary text-primary',
|
||||||
|
'hover:bg-primary/10',
|
||||||
|
'aria-checked:ring-3 aria-checked: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',
|
||||||
|
'dark:bg-input/30 shadow-xs'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
sm: 'size-4',
|
||||||
|
md: 'size-5',
|
||||||
|
lg: 'size-6'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'md'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
return <RadioGroupPrimitive.Root data-slot="radio-group" className={cn('grid gap-3', className)} {...props} />
|
return <RadioGroupPrimitive.Root data-slot="radio-group" className={cn('grid gap-3', className)} {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
size = 'md',
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item> & VariantProps<typeof radioGroupItemVariants>) {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
data-slot="radio-group-item"
|
data-slot="radio-group-item"
|
||||||
className={cn(
|
data-size={size}
|
||||||
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
className={cn(radioGroupItemVariants({ size }), className)}
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}>
|
{...props}>
|
||||||
<RadioGroupPrimitive.Indicator
|
<RadioGroupPrimitive.Indicator
|
||||||
data-slot="radio-group-indicator"
|
data-slot="radio-group-indicator"
|
||||||
className="relative flex items-center justify-center">
|
className="relative flex items-center justify-center">
|
||||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
<CircleIcon
|
||||||
|
className={cn('fill-primary absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-2.5')}
|
||||||
|
/>
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
export { RadioGroup, RadioGroupItem, radioGroupItemVariants }
|
||||||
|
|||||||
534
packages/ui/stories/components/primitives/RadioGroup.stories.tsx
Normal file
534
packages/ui/stories/components/primitives/RadioGroup.stories.tsx
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
import { Bell, Check, Moon, Palette, Sun } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { RadioGroup, RadioGroupItem } from '../../../src/components/primitives/radioGroup'
|
||||||
|
|
||||||
|
const meta: Meta<typeof RadioGroup> = {
|
||||||
|
title: 'Components/Primitives/RadioGroup',
|
||||||
|
component: RadioGroup,
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'A radio group component based on Radix UI, allowing users to select a single option from a set. Supports three sizes (sm, md, lg) as defined in the Figma design system.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
disabled: {
|
||||||
|
control: { type: 'boolean' },
|
||||||
|
description: 'Whether the radio group is disabled'
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
control: { type: 'text' },
|
||||||
|
description: 'Default selected value'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
control: { type: 'text' },
|
||||||
|
description: 'Value in controlled mode'
|
||||||
|
},
|
||||||
|
orientation: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['horizontal', 'vertical'],
|
||||||
|
description: 'The orientation of the radio group'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
// Default
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<RadioGroup defaultValue="option1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option1" id="option1" />
|
||||||
|
<label htmlFor="option1" className="cursor-pointer text-sm">
|
||||||
|
Option 1
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option2" id="option2" />
|
||||||
|
<label htmlFor="option2" className="cursor-pointer text-sm">
|
||||||
|
Option 2
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option3" id="option3" />
|
||||||
|
<label htmlFor="option3" className="cursor-pointer text-sm">
|
||||||
|
Option 3
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With Default Value
|
||||||
|
export const WithDefaultValue: Story = {
|
||||||
|
render: () => (
|
||||||
|
<RadioGroup defaultValue="option2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option1" id="default-option1" />
|
||||||
|
<label htmlFor="default-option1" className="cursor-pointer text-sm">
|
||||||
|
Option 1
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option2" id="default-option2" />
|
||||||
|
<label htmlFor="default-option2" className="cursor-pointer text-sm">
|
||||||
|
Option 2 (Default)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option3" id="default-option3" />
|
||||||
|
<label htmlFor="default-option3" className="cursor-pointer text-sm">
|
||||||
|
Option 3
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal Layout
|
||||||
|
export const HorizontalLayout: Story = {
|
||||||
|
render: () => (
|
||||||
|
<RadioGroup defaultValue="option1" className="flex-row">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option1" id="h-option1" />
|
||||||
|
<label htmlFor="h-option1" className="cursor-pointer text-sm">
|
||||||
|
Option 1
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option2" id="h-option2" />
|
||||||
|
<label htmlFor="h-option2" className="cursor-pointer text-sm">
|
||||||
|
Option 2
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option3" id="h-option3" />
|
||||||
|
<label htmlFor="h-option3" className="cursor-pointer text-sm">
|
||||||
|
Option 3
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
export const Disabled: Story = {
|
||||||
|
render: () => (
|
||||||
|
<RadioGroup disabled defaultValue="option1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option1" id="disabled-option1" />
|
||||||
|
<label htmlFor="disabled-option1" className="cursor-not-allowed text-sm opacity-50">
|
||||||
|
Option 1 (Selected & Disabled)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option2" id="disabled-option2" />
|
||||||
|
<label htmlFor="disabled-option2" className="cursor-not-allowed text-sm opacity-50">
|
||||||
|
Option 2 (Disabled)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option3" id="disabled-option3" />
|
||||||
|
<label htmlFor="disabled-option3" className="cursor-not-allowed text-sm opacity-50">
|
||||||
|
Option 3 (Disabled)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled Items
|
||||||
|
export const DisabledItems: Story = {
|
||||||
|
render: () => (
|
||||||
|
<RadioGroup defaultValue="option1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option1" id="partial-option1" />
|
||||||
|
<label htmlFor="partial-option1" className="cursor-pointer text-sm">
|
||||||
|
Option 1
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option2" id="partial-option2" disabled />
|
||||||
|
<label htmlFor="partial-option2" className="cursor-not-allowed text-sm opacity-50">
|
||||||
|
Option 2 (Disabled)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option3" id="partial-option3" />
|
||||||
|
<label htmlFor="partial-option3" className="cursor-pointer text-sm">
|
||||||
|
Option 3
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option4" id="partial-option4" disabled />
|
||||||
|
<label htmlFor="partial-option4" className="cursor-not-allowed text-sm opacity-50">
|
||||||
|
Option 4 (Disabled)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With Descriptions
|
||||||
|
export const WithDescriptions: Story = {
|
||||||
|
render: () => (
|
||||||
|
<RadioGroup defaultValue="plan1" className="gap-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<RadioGroupItem value="plan1" id="plan1" className="mt-1" />
|
||||||
|
<label htmlFor="plan1" className="cursor-pointer">
|
||||||
|
<div className="text-sm font-medium">Free Plan</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Perfect for getting started</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<RadioGroupItem value="plan2" id="plan2" className="mt-1" />
|
||||||
|
<label htmlFor="plan2" className="cursor-pointer">
|
||||||
|
<div className="text-sm font-medium">Pro Plan</div>
|
||||||
|
<div className="text-xs text-muted-foreground">For professional developers</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<RadioGroupItem value="plan3" id="plan3" className="mt-1" />
|
||||||
|
<label htmlFor="plan3" className="cursor-pointer">
|
||||||
|
<div className="text-sm font-medium">Enterprise Plan</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Advanced features for teams</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controlled
|
||||||
|
export const Controlled: Story = {
|
||||||
|
render: function ControlledExample() {
|
||||||
|
const [value, setValue] = useState('option1')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<RadioGroup value={value} onValueChange={setValue}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option1" id="controlled-option1" />
|
||||||
|
<label htmlFor="controlled-option1" className="cursor-pointer text-sm">
|
||||||
|
Option 1
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option2" id="controlled-option2" />
|
||||||
|
<label htmlFor="controlled-option2" className="cursor-pointer text-sm">
|
||||||
|
Option 2
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option3" id="controlled-option3" />
|
||||||
|
<label htmlFor="controlled-option3" className="cursor-pointer text-sm">
|
||||||
|
Option 3
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
<div className="text-sm text-muted-foreground">Current value: {value}</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>
|
||||||
|
<RadioGroup defaultValue="sm1" className="gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="sm1" id="size-sm-1" size="sm" />
|
||||||
|
<label htmlFor="size-sm-1" className="cursor-pointer text-sm">
|
||||||
|
Small Radio
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="sm2" id="size-sm-2" size="sm" />
|
||||||
|
<label htmlFor="size-sm-2" className="cursor-pointer text-sm">
|
||||||
|
Small Radio
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="mb-3 text-sm text-muted-foreground">Medium (md) - Default</p>
|
||||||
|
<RadioGroup defaultValue="md1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="md1" id="size-md-1" size="md" />
|
||||||
|
<label htmlFor="size-md-1" className="cursor-pointer text-sm">
|
||||||
|
Medium Radio
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="md2" id="size-md-2" size="md" />
|
||||||
|
<label htmlFor="size-md-2" className="cursor-pointer text-sm">
|
||||||
|
Medium Radio
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="mb-3 text-sm text-muted-foreground">Large (lg)</p>
|
||||||
|
<RadioGroup defaultValue="lg1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="lg1" id="size-lg-1" size="lg" />
|
||||||
|
<label htmlFor="size-lg-1" className="cursor-pointer text-sm">
|
||||||
|
Large Radio
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="lg2" id="size-lg-2" size="lg" />
|
||||||
|
<label htmlFor="size-lg-2" className="cursor-pointer text-sm">
|
||||||
|
Large Radio
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All States
|
||||||
|
export const AllStates: Story = {
|
||||||
|
render: function AllStatesExample() {
|
||||||
|
const [normalValue, setNormalValue] = useState('')
|
||||||
|
const [selectedValue, setSelectedValue] = useState('option2')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{/* Normal State */}
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">Normal State</p>
|
||||||
|
<RadioGroup value={normalValue} onValueChange={setNormalValue}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option1" id="state-normal-1" />
|
||||||
|
<label htmlFor="state-normal-1" className="cursor-pointer text-sm">
|
||||||
|
Unselected Option
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected State */}
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">Selected State</p>
|
||||||
|
<RadioGroup value={selectedValue} onValueChange={setSelectedValue}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option2" id="state-selected-2" />
|
||||||
|
<label htmlFor="state-selected-2" className="cursor-pointer text-sm">
|
||||||
|
Selected Option
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Disabled State */}
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">Disabled State</p>
|
||||||
|
<RadioGroup disabled value={selectedValue}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option2" id="state-disabled-2" />
|
||||||
|
<label htmlFor="state-disabled-2" className="cursor-not-allowed text-sm opacity-50">
|
||||||
|
Disabled (Selected)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-sm text-muted-foreground">Error State</p>
|
||||||
|
<RadioGroup value="">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="option1" id="state-error-1" aria-invalid />
|
||||||
|
<label htmlFor="state-error-1" className="cursor-pointer text-sm">
|
||||||
|
Option (Required)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
<p className="mt-1 text-xs text-destructive">Please select an option</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real World Examples
|
||||||
|
export const RealWorldExamples: Story = {
|
||||||
|
render: function RealWorldExample() {
|
||||||
|
const [theme, setTheme] = useState('light')
|
||||||
|
const [notifications, setNotifications] = useState('all')
|
||||||
|
const [visibility, setVisibility] = useState('public')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
{/* Theme Selection */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-sm font-semibold">Theme Preference</h3>
|
||||||
|
<RadioGroup value={theme} onValueChange={setTheme} className="gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="light" id="theme-light" />
|
||||||
|
<label htmlFor="theme-light" className="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
|
<Sun className="size-4" />
|
||||||
|
Light Mode
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="dark" id="theme-dark" />
|
||||||
|
<label htmlFor="theme-dark" className="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
|
<Moon className="size-4" />
|
||||||
|
Dark Mode
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem value="system" id="theme-system" />
|
||||||
|
<label htmlFor="theme-system" className="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
|
<Palette className="size-4" />
|
||||||
|
System Default
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notification Settings */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-sm font-semibold">Notification Settings</h3>
|
||||||
|
<RadioGroup value={notifications} onValueChange={setNotifications} className="gap-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<RadioGroupItem value="all" id="notif-all" className="mt-1" />
|
||||||
|
<label htmlFor="notif-all" className="cursor-pointer">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<Bell className="size-4" />
|
||||||
|
All Notifications
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Receive all notifications and updates</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<RadioGroupItem value="important" id="notif-important" className="mt-1" />
|
||||||
|
<label htmlFor="notif-important" className="cursor-pointer">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<Check className="size-4" />
|
||||||
|
Important Only
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Only receive critical notifications</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<RadioGroupItem value="none" id="notif-none" className="mt-1" />
|
||||||
|
<label htmlFor="notif-none" className="cursor-pointer">
|
||||||
|
<div className="text-sm font-medium">None</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Turn off all notifications</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visibility Settings */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-sm font-semibold">Profile Visibility</h3>
|
||||||
|
<RadioGroup value={visibility} onValueChange={setVisibility}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="public" id="vis-public" />
|
||||||
|
<label htmlFor="vis-public" className="cursor-pointer text-sm">
|
||||||
|
Public - Anyone can see your profile
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="friends" id="vis-friends" />
|
||||||
|
<label htmlFor="vis-friends" className="cursor-pointer text-sm">
|
||||||
|
Friends Only - Only your friends can see
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="private" id="vis-private" />
|
||||||
|
<label htmlFor="vis-private" className="cursor-pointer text-sm">
|
||||||
|
Private - Only you can see
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Required Field Example */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-sm font-semibold">
|
||||||
|
Payment Method <span className="text-destructive">*</span>
|
||||||
|
</h3>
|
||||||
|
<RadioGroup value="">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="credit" id="pay-credit" aria-invalid />
|
||||||
|
<label htmlFor="pay-credit" className="cursor-pointer text-sm">
|
||||||
|
Credit Card
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="paypal" id="pay-paypal" aria-invalid />
|
||||||
|
<label htmlFor="pay-paypal" className="cursor-pointer text-sm">
|
||||||
|
PayPal
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RadioGroupItem value="bank" id="pay-bank" aria-invalid />
|
||||||
|
<label htmlFor="pay-bank" className="cursor-pointer text-sm">
|
||||||
|
Bank Transfer
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
<p className="mt-1 text-xs text-destructive">Please select a payment method</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card Style
|
||||||
|
export const CardStyle: Story = {
|
||||||
|
render: () => (
|
||||||
|
<RadioGroup defaultValue="starter" className="gap-4">
|
||||||
|
<label
|
||||||
|
htmlFor="card-starter"
|
||||||
|
className="flex cursor-pointer items-start gap-3 rounded-lg border border-input p-4 transition-colors hover:bg-accent/50 has-[:checked]:border-primary has-[:checked]:bg-accent">
|
||||||
|
<RadioGroupItem value="starter" id="card-starter" className="mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-semibold">Starter</div>
|
||||||
|
<div className="text-xs text-muted-foreground">Best for individual use</div>
|
||||||
|
<div className="mt-2 text-lg font-bold">$9/month</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
htmlFor="card-pro"
|
||||||
|
className="flex cursor-pointer items-start gap-3 rounded-lg border border-input p-4 transition-colors hover:bg-accent/50 has-[:checked]:border-primary has-[:checked]:bg-accent">
|
||||||
|
<RadioGroupItem value="pro" id="card-pro" className="mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-semibold">Pro</div>
|
||||||
|
<div className="text-xs text-muted-foreground">For professional developers</div>
|
||||||
|
<div className="mt-2 text-lg font-bold">$29/month</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
htmlFor="card-enterprise"
|
||||||
|
className="flex cursor-pointer items-start gap-3 rounded-lg border border-input p-4 transition-colors hover:bg-accent/50 has-[:checked]:border-primary has-[:checked]:bg-accent">
|
||||||
|
<RadioGroupItem value="enterprise" id="card-enterprise" className="mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-semibold">Enterprise</div>
|
||||||
|
<div className="text-xs text-muted-foreground">For large teams</div>
|
||||||
|
<div className="mt-2 text-lg font-bold">Custom pricing</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</RadioGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user