Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/components/ui/checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,34 @@ export const Disabled: Story = {
expect(canvas.getByText("Email me when the build completes")).toBeInTheDocument()
})
},
}
}
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)
})
},
}
35 changes: 26 additions & 9 deletions src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof CheckboxPrimitive.Root>) {
}: React.ComponentProps<typeof CheckboxPrimitive.Root> &
VariantProps<typeof checkboxVariants>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 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",
className
)}
data-size={size ?? "default"}
className={cn(checkboxVariants({ size }), className)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5 group-data-[size=xs]/checkbox:[&>svg]:size-3 group-data-[size=lg]/checkbox:[&>svg]:size-4"
>
<CheckIcon
/>
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}

export { Checkbox }
export { Checkbox, checkboxVariants }
42 changes: 41 additions & 1 deletion src/components/ui/radio-group.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,44 @@ export const DisabledOption: Story = {
expect(canvas.getByRole("radio", { name: /Enterprise/i })).toBeDisabled()
})
},
}
}
export const ExtraSmall: Story = {
render: () => (
<RadioGroup size="xs" defaultValue="team" aria-label="Plan">
<RadioGroupItem value="starter" aria-label="Starter" />
<RadioGroupItem value="team" aria-label="Team" />
</RadioGroup>
),
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: () => (
<RadioGroup size="lg" defaultValue="team" aria-label="Plan">
<RadioGroupItem value="starter" aria-label="Starter" />
<RadioGroupItem value="team" aria-label="Team" />
</RadioGroup>
),
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)
})
},
}
56 changes: 42 additions & 14 deletions src/components/ui/radio-group.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof RadioGroupPrimitive.Root>) {
}: React.ComponentProps<typeof RadioGroupPrimitive.Root> & { size?: RadioSize }) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid w-full gap-2", className)}
{...props}
/>
<RadioGroupContext.Provider value={{ size: size ?? "default" }}>
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid w-full gap-2", className)}
{...props}
/>
</RadioGroupContext.Provider>
)
}

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<typeof RadioGroupPrimitive.Item>) {
}: React.ComponentProps<typeof RadioGroupPrimitive.Item> &
VariantProps<typeof radioGroupItemVariants>) {
const context = React.useContext(RadioGroupContext)
const resolved = size ?? context.size ?? "default"
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"group/radio-group-item peer relative flex aspect-square size-4 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",
className
)}
data-size={resolved}
className={cn(radioGroupItemVariants({ size: resolved }), className)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="flex size-4 items-center justify-center"
className="flex size-full items-center justify-center"
>
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground group-data-[size=xs]/radio-group-item:size-1.5 group-data-[size=lg]/radio-group-item:size-2.5" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}

export { RadioGroup, RadioGroupItem }
export { RadioGroup, RadioGroupItem, radioGroupItemVariants }
Loading