diff --git a/src/components/ui/checkbox.stories.tsx b/src/components/ui/checkbox.stories.tsx index 22d27f48..159a5bc9 100644 --- a/src/components/ui/checkbox.stories.tsx +++ b/src/components/ui/checkbox.stories.tsx @@ -80,4 +80,34 @@ export const Disabled: Story = { expect(canvas.getByText("Email me when the build completes")).toBeInTheDocument() }) }, -} \ No newline at end of file +} +export const ExtraSmall: Story = { + render: () => renderCheckbox({ size: "xs", defaultChecked: true }), + parameters: { + zephyr: { testCaseId: "SW-T5693" }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("xs checkbox box is 14px", async () => { + const cb = canvas.getByRole("checkbox") + expect(cb).toHaveAttribute("data-size", "xs") + expect(Math.round(cb.getBoundingClientRect().height)).toBe(14) + }) + }, +} + +export const Large: Story = { + render: () => renderCheckbox({ size: "lg", defaultChecked: true }), + parameters: { + zephyr: { testCaseId: "SW-T5694" }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("lg checkbox box is 20px", async () => { + const cb = canvas.getByRole("checkbox") + expect(Math.round(cb.getBoundingClientRect().height)).toBe(20) + }) + }, +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index cc790fa8..10e904d0 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -1,32 +1,49 @@ +import { cva, type VariantProps } from "class-variance-authority" import { CheckIcon } from "lucide-react" import { Checkbox as CheckboxPrimitive } from "radix-ui" import * as React from "react" import { cn } from "@/lib/utils" +const checkboxVariants = cva( + "group/checkbox peer relative flex shrink-0 items-center justify-center rounded-[4px] border border-input bg-card transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-focus aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", + { + variants: { + // Box scale (SW-2591): default stays 16 (no visual change); xs 14 / lg 20. + size: { + xs: "size-3.5", + sm: "size-4", + default: "size-4", + lg: "size-5", + }, + }, + defaultVariants: { + size: "default", + }, + } +) function Checkbox({ className, + size, ...props -}: React.ComponentProps) { +}: React.ComponentProps & + VariantProps) { return ( - + ) } -export { Checkbox } +export { Checkbox, checkboxVariants } diff --git a/src/components/ui/radio-group.stories.tsx b/src/components/ui/radio-group.stories.tsx index 595974f0..b2e01007 100644 --- a/src/components/ui/radio-group.stories.tsx +++ b/src/components/ui/radio-group.stories.tsx @@ -96,4 +96,44 @@ export const DisabledOption: Story = { expect(canvas.getByRole("radio", { name: /Enterprise/i })).toBeDisabled() }) }, -} \ No newline at end of file +} +export const ExtraSmall: Story = { + render: () => ( + + + + + ), + parameters: { + zephyr: { testCaseId: "SW-T5695" }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("xs radio box is 14px (group size flows to items)", async () => { + const item = canvas.getAllByRole("radio")[0] + expect(item).toHaveAttribute("data-size", "xs") + expect(Math.round(item.getBoundingClientRect().height)).toBe(14) + }) + }, +} + +export const Large: Story = { + render: () => ( + + + + + ), + parameters: { + zephyr: { testCaseId: "SW-T5696" }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + + await step("lg radio box is 20px", async () => { + const item = canvas.getAllByRole("radio")[0] + expect(Math.round(item.getBoundingClientRect().height)).toBe(20) + }) + }, +} diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx index 174c3c12..83d3d1be 100644 --- a/src/components/ui/radio-group.tsx +++ b/src/components/ui/radio-group.tsx @@ -1,44 +1,72 @@ "use client" +import { cva, type VariantProps } from "class-variance-authority" import { RadioGroup as RadioGroupPrimitive } from "radix-ui" import * as React from "react" import { cn } from "@/lib/utils" +type RadioSize = "xs" | "sm" | "default" | "lg" + +const RadioGroupContext = React.createContext<{ size?: RadioSize }>({}) + function RadioGroup({ className, + size, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { size?: RadioSize }) { return ( - + + + ) } +const radioGroupItemVariants = cva( + "group/radio-group-item peer relative flex aspect-square shrink-0 rounded-full border border-input bg-card outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-focus aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", + { + variants: { + // Box scale (SW-2591): default stays 16 (no visual change); xs 14 / lg 20. + size: { + xs: "size-3.5", + sm: "size-4", + default: "size-4", + lg: "size-5", + }, + }, + defaultVariants: { + size: "default", + }, + } +) + function RadioGroupItem({ className, + size, ...props -}: React.ComponentProps) { +}: React.ComponentProps & + VariantProps) { + const context = React.useContext(RadioGroupContext) + const resolved = size ?? context.size ?? "default" return ( - + ) } -export { RadioGroup, RadioGroupItem } +export { RadioGroup, RadioGroupItem, radioGroupItemVariants }