diff --git a/libs/ui/package.json b/libs/ui/package.json index 079b34af7..6f6a46d79 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -78,6 +78,7 @@ "@zag-js/number-input": "^1.31.1", "@zag-js/pagination": "^1.31.1", "@zag-js/popover": "^1.31.1", + "@zag-js/radio-group": "^1.31.1", "@zag-js/rating-group": "^1.31.1", "@zag-js/react": "^1.31.1", "@zag-js/select": "^1.31.1", diff --git a/libs/ui/src/molecules/radio-group.tsx b/libs/ui/src/molecules/radio-group.tsx new file mode 100644 index 000000000..7720ac1cb --- /dev/null +++ b/libs/ui/src/molecules/radio-group.tsx @@ -0,0 +1,501 @@ +import * as zagRadioGroup from "@zag-js/radio-group" +import { normalizeProps, useMachine } from "@zag-js/react" +import { + type ComponentPropsWithoutRef, + createContext, + type ReactNode, + type Ref, + useContext, + useId, +} from "react" +import type { VariantProps } from "tailwind-variants" +import { Label } from "../atoms/label" +import { StatusText } from "../atoms/status-text" +import { tv } from "../utils" + +const radioGroupVariants = tv({ + slots: { + root: ["flex w-full flex-col"], + itemGroup: [ + "relative flex", + "data-[orientation=horizontal]:flex-row", + "data-[orientation=horizontal]:flex-wrap", + "data-[orientation=vertical]:flex-col", + ], + item: [ + "grid grid-cols-(--radio-group-item-grid) items-start", + "cursor-pointer select-none", + "data-disabled:cursor-not-allowed", + "data-readonly:cursor-default", + ], + itemControl: [ + "row-start-1 self-center", + "inline-grid shrink-0 place-items-center rounded-radio-group-control", + "border-(length:--border-width-radio-group)", + "border-radio-group-item-control-border", + "bg-radio-group-item-control-bg", + "transition-colors duration-200 motion-reduce:transition-none", + "data-hover:bg-radio-group-item-control-bg-hover", + "data-hover:border-radio-group-item-control-border-hover", + "data-disabled:bg-radio-group-item-control-bg-disabled", + "data-disabled:border-radio-group-item-control-border-disabled", + "data-focus-visible:outline-(style:--default-ring-style)", + "data-focus-visible:outline-(length:--default-ring-width)", + "data-focus-visible:outline-radio-group-ring", + "data-focus-visible:outline-offset-(length:--default-ring-offset)", + "data-invalid:border-radio-group-item-control-border-error", + "data-invalid:outline-offset-(length:--default-ring-offset)", + ], + itemContent: ["col-start-2 row-start-1 min-w-0 flex flex-col"], + itemIndicator: [ + "pointer-events-none block leading-none", + "token-icon-radio-group-checked", + "opacity-0 transition-opacity duration-200 motion-reduce:transition-none", + "data-[state=checked]:opacity-100", + "data-disabled:data-[state=checked]:text-radio-group-item-indicator-disabled", + ], + itemText: [ + "min-w-0 text-radio-group-item-fg leading-normal", + "data-disabled:text-radio-group-item-fg-disabled", + ], + itemDescription: [ + "col-start-2 row-start-2 min-w-0 text-radio-group-item-description leading-normal", + "data-disabled:text-radio-group-item-description-disabled", + ], + hiddenInput: "sr-only", + }, + variants: { + variant: { + outline: { + itemControl: [ + "data-[state=checked]:bg-radio-group-item-control-bg-outline-checked", + "data-[state=checked]:border-radio-group-item-control-border-outline-checked", + "data-hover:data-[state=checked]:bg-radio-group-item-control-bg-outline-checked-hover", + "data-hover:data-[state=checked]:border-radio-group-item-control-border-outline-checked-hover", + ], + itemIndicator: "text-radio-group-item-indicator-outline", + }, + subtle: { + itemControl: [ + "data-[state=checked]:bg-radio-group-item-control-bg-subtle-checked", + "data-[state=checked]:border-radio-group-item-control-border-subtle-checked", + "data-hover:data-[state=checked]:bg-radio-group-item-control-bg-subtle-checked-hover", + "data-hover:data-[state=checked]:border-radio-group-item-control-border-subtle-checked-hover", + ], + itemIndicator: "text-radio-group-item-indicator-subtle", + }, + solid: { + itemControl: [ + "data-[state=checked]:bg-radio-group-item-control-bg-solid-checked", + "data-[state=checked]:border-radio-group-item-control-border-solid-checked", + "data-hover:data-[state=checked]:bg-radio-group-item-control-bg-solid-checked-hover", + "data-hover:data-[state=checked]:border-radio-group-item-control-border-solid-checked-hover", + ], + itemIndicator: "text-radio-group-item-indicator-solid", + }, + }, + size: { + sm: { + root: "gap-radio-group-root-sm", + itemGroup: + "data-[orientation=horizontal]:gap-radio-group-items-horizontal-sm data-[orientation=vertical]:gap-radio-group-items-vertical-sm", + item: "gap-x-radio-group-item-sm", + itemControl: "size-radio-group-control-sm", + itemContent: "gap-radio-group-item-content-sm", + itemIndicator: "size-radio-group-indicator-sm", + itemText: "text-radio-group-item-sm", + itemDescription: "text-radio-group-item-description-sm", + }, + md: { + root: "gap-radio-group-root-md", + itemGroup: + "data-[orientation=horizontal]:gap-radio-group-items-horizontal-md data-[orientation=vertical]:gap-radio-group-items-vertical-md", + item: "gap-x-radio-group-item-md", + itemControl: "size-radio-group-control-md", + itemContent: "gap-radio-group-item-content-md", + itemIndicator: "size-radio-group-indicator-md", + itemText: "text-radio-group-item-md", + itemDescription: "text-radio-group-item-description-md", + }, + lg: { + root: "gap-radio-group-root-lg", + itemGroup: + "data-[orientation=horizontal]:gap-radio-group-items-horizontal-lg data-[orientation=vertical]:gap-radio-group-items-vertical-lg", + item: "gap-x-radio-group-item-lg", + itemControl: "size-radio-group-control-lg", + itemContent: "gap-radio-group-item-content-lg", + itemIndicator: "size-radio-group-indicator-lg", + itemText: "text-radio-group-item-lg", + itemDescription: "text-radio-group-item-description-lg", + }, + }, + }, + defaultVariants: { + variant: "outline", + size: "md", + }, +}) + +type RadioGroupVariant = NonNullable< + VariantProps["variant"] +> + +type RadioGroupSize = NonNullable< + VariantProps["size"] +> + +type RadioGroupValidateStatus = "default" | "error" | "success" | "warning" + +type RadioGroupContextValue = { + api: ReturnType + variant: RadioGroupVariant + size: RadioGroupSize + orientation: "horizontal" | "vertical" + disabled: boolean + required: boolean + validateStatus: RadioGroupValidateStatus +} + +const RadioGroupContext = createContext(null) + +function useRadioGroupContext() { + const context = useContext(RadioGroupContext) + if (!context) { + throw new Error("RadioGroup components must be used within RadioGroup") + } + return context +} + +type RadioGroupItemContextValue = { + itemProps: zagRadioGroup.ItemProps +} + +const RadioGroupItemContext = + createContext(null) + +function useRadioGroupItemContext() { + const context = useContext(RadioGroupItemContext) + if (!context) { + throw new Error("RadioGroup item components must be used within RadioGroup.Item") + } + return context +} + +type RadioGroupMachineProps = Omit< + zagRadioGroup.Props, + "id" | "invalid" | "onValueChange" +> + +export type RadioGroupProps = VariantProps & + RadioGroupMachineProps & { + id?: string + children: ReactNode + className?: string + ref?: Ref + validateStatus?: RadioGroupValidateStatus + onValueChange?: (value: string | null) => void + } + +export function RadioGroup({ + id: providedId, + disabled = false, + required = false, + orientation = "vertical", + validateStatus = "default", + onValueChange, + variant = "outline", + size = "md", + children, + className, + ref, + ...machineProps +}: RadioGroupProps) { + const generatedId = useId() + const id = providedId || generatedId + const invalid = validateStatus === "error" + + const service = useMachine(zagRadioGroup.machine, { + ...machineProps, + id, + disabled, + required, + orientation, + invalid, + onValueChange: ({ value: nextValue }: zagRadioGroup.ValueChangeDetails) => { + onValueChange?.(nextValue) + }, + }) + + const api = zagRadioGroup.connect( + service, + normalizeProps, + ) + const styles = radioGroupVariants({ size, variant }) + + return ( + +
+ {children} +
+
+ ) +} + +type RadioGroupLabelProps = Omit< + ComponentPropsWithoutRef, + "disabled" | "required" +> & { + disabled?: boolean + required?: boolean + ref?: Ref +} + +RadioGroup.Label = function RadioGroupLabel({ + children, + disabled, + required, + size: sizeProp, + ...props +}: RadioGroupLabelProps) { + const { api, size, disabled: groupDisabled, required: groupRequired } = + useRadioGroupContext() + + return ( + + ) +} + +type RadioGroupItemGroupProps = ComponentPropsWithoutRef<"div"> & { + ref?: Ref +} + +RadioGroup.ItemGroup = function RadioGroupItemGroup({ + children, + className, + ref, + ...props +}: RadioGroupItemGroupProps) { + const { size, variant, orientation } = useRadioGroupContext() + const styles = radioGroupVariants({ size, variant }) + + return ( +
+ {children} +
+ ) +} + +export type RadioGroupItemProps = Omit, "value"> & + zagRadioGroup.ItemProps & { + ref?: Ref + } + +RadioGroup.Item = function RadioGroupItem({ + value, + disabled, + invalid, + children, + className, + ref, + ...props +}: RadioGroupItemProps) { + const { api, size, variant } = useRadioGroupContext() + const styles = radioGroupVariants({ size, variant }) + const itemProps = { value, disabled, invalid } + + return ( + + + + ) +} + +type RadioGroupItemHiddenInputProps = Omit< + ComponentPropsWithoutRef<"input">, + "type" | "value" +> & { + ref?: Ref +} + +RadioGroup.ItemHiddenInput = function RadioGroupItemHiddenInput({ + className, + ref, + ...props +}: RadioGroupItemHiddenInputProps) { + const { api, size, variant } = useRadioGroupContext() + const { itemProps } = useRadioGroupItemContext() + const styles = radioGroupVariants({ size, variant }) + + return ( + + ) +} + +type RadioGroupItemControlProps = ComponentPropsWithoutRef<"span"> & { + ref?: Ref +} + +RadioGroup.ItemControl = function RadioGroupItemControl({ + children, + className, + ref, + ...props +}: RadioGroupItemControlProps) { + const { api, size, variant } = useRadioGroupContext() + const { itemProps } = useRadioGroupItemContext() + const styles = radioGroupVariants({ size, variant }) + const itemState = api.getItemState(itemProps) + + return ( + + + ) +} + +type RadioGroupItemContentProps = ComponentPropsWithoutRef<"div"> & { + ref?: Ref +} + +RadioGroup.ItemContent = function RadioGroupItemContent({ + children, + className, + ref, + ...props +}: RadioGroupItemContentProps) { + const { size, variant } = useRadioGroupContext() + const styles = radioGroupVariants({ size, variant }) + + return ( +
+ {children} +
+ ) +} + +type RadioGroupItemTextProps = ComponentPropsWithoutRef<"span"> & { + ref?: Ref +} + +RadioGroup.ItemText = function RadioGroupItemText({ + children, + className, + ref, + ...props +}: RadioGroupItemTextProps) { + const { api, size, variant } = useRadioGroupContext() + const { itemProps } = useRadioGroupItemContext() + const styles = radioGroupVariants({ size, variant }) + + return ( + + {children} + + ) +} + +type RadioGroupItemDescriptionProps = ComponentPropsWithoutRef<"div"> & { + ref?: Ref +} + +RadioGroup.ItemDescription = function RadioGroupItemDescription({ + children, + className, + ref, + ...props +}: RadioGroupItemDescriptionProps) { + const { api, size, variant } = useRadioGroupContext() + const { itemProps } = useRadioGroupItemContext() + const styles = radioGroupVariants({ size, variant }) + const itemState = api.getItemState(itemProps) + + return ( +
+ {children} +
+ ) +} + +type RadioGroupStatusTextProps = Omit< + ComponentPropsWithoutRef, + "status" | "size" +> & { + status?: RadioGroupValidateStatus + size?: RadioGroupSize + ref?: Ref +} + +RadioGroup.StatusText = function RadioGroupStatusText({ + status, + size: sizeProp, + showIcon, + children, + ...props +}: RadioGroupStatusTextProps) { + const { size, validateStatus } = useRadioGroupContext() + const effectiveSize = sizeProp ?? size + const effectiveStatus = status ?? validateStatus + + return ( + + {children} + + ) +} + +export { radioGroupVariants, useRadioGroupContext } + +RadioGroup.displayName = "RadioGroup" diff --git a/libs/ui/src/tokens/components/components.css b/libs/ui/src/tokens/components/components.css index c4bfd8a7d..e6fc8a680 100644 --- a/libs/ui/src/tokens/components/components.css +++ b/libs/ui/src/tokens/components/components.css @@ -22,6 +22,7 @@ @import "./molecules/_dialog.css"; @import "./molecules/_pagination.css"; @import "./molecules/_popover.css"; +@import "./molecules/_radio-group.css"; @import "./molecules/_menu.css"; @import "./molecules/_tabs.css"; @import "./molecules/_product-card.css"; diff --git a/libs/ui/src/tokens/components/molecules/_radio-group.css b/libs/ui/src/tokens/components/molecules/_radio-group.css new file mode 100644 index 000000000..336db6c2e --- /dev/null +++ b/libs/ui/src/tokens/components/molecules/_radio-group.css @@ -0,0 +1,150 @@ +@theme static { + --radio-group-item-grid: min-content minmax(0, 1fr); + + /* === BASE COLOR MAPPING === */ + --color-radio-group-accent: var(--color-primary); + --color-radio-group-accent-hover: var(--color-primary-hover); + --color-radio-group-accent-light: var(--color-primary-light); + --color-radio-group-accent-light-hover: var(--color-primary-light-hover); + --color-radio-group-accent-contrast: var(--color-fg-dark); + --color-radio-group-danger: var(--color-danger); + --color-radio-group-fg: var(--color-fg-primary); + --color-radio-group-fg-secondary: var(--color-fg-secondary); + --color-radio-group-fg-disabled: var(--color-fg-disabled); + --color-radio-group-surface: var(--color-fill-active); + --color-radio-group-border: var(--color-border-primary); + + /* === DERIVED COLORS === */ + --color-radio-group-item-control-bg: var(--color-radio-group-surface); + --color-radio-group-item-control-border: var(--color-radio-group-border); + --color-radio-group-item-indicator: var(--color-radio-group-accent); + --color-radio-group-item-fg: var(--color-radio-group-fg); + --color-radio-group-item-description: var(--color-radio-group-fg-secondary); + + /* === STATE VARIATIONS === */ + --color-radio-group-item-control-bg-hover: oklch( + from var(--color-radio-group-item-control-bg) calc(l + var(--state-hover)) c + h + ); + --color-radio-group-item-control-border-hover: oklch( + from var(--color-radio-group-item-control-border) + calc(l + var(--state-hover)) c h + ); + + /* === COMPONENT VARIANTS === */ + --color-radio-group-item-control-bg-outline-checked: var( + --color-radio-group-item-control-bg + ); + --color-radio-group-item-control-bg-outline-checked-hover: var( + --color-radio-group-item-control-bg-hover + ); + --color-radio-group-item-control-border-outline-checked: var( + --color-radio-group-accent + ); + --color-radio-group-item-control-border-outline-checked-hover: var( + --color-radio-group-accent-hover + ); + --color-radio-group-item-indicator-outline: var( + --color-radio-group-item-indicator + ); + + --color-radio-group-item-control-bg-subtle-checked: var( + --color-radio-group-accent-light + ); + --color-radio-group-item-control-bg-subtle-checked-hover: var( + --color-radio-group-accent-light-hover + ); + --color-radio-group-item-control-border-subtle-checked: var( + --color-radio-group-accent-light + ); + --color-radio-group-item-control-border-subtle-checked-hover: var( + --color-radio-group-accent-light-hover + ); + --color-radio-group-item-indicator-subtle: var( + --color-radio-group-item-indicator + ); + + --color-radio-group-item-control-bg-solid-checked: var( + --color-radio-group-accent + ); + --color-radio-group-item-control-bg-solid-checked-hover: var( + --color-radio-group-accent-hover + ); + --color-radio-group-item-control-border-solid-checked: var( + --color-radio-group-accent + ); + --color-radio-group-item-control-border-solid-checked-hover: var( + --color-radio-group-accent-hover + ); + --color-radio-group-item-indicator-solid: var( + --color-radio-group-accent-contrast + ); + + /* === VALIDATION STATES === */ + --color-radio-group-item-control-border-error: var( + --color-radio-group-danger + ); + --color-radio-group-ring-error: var(--color-radio-group-danger); + + /* === DISABLED STATES === */ + --color-radio-group-item-control-bg-disabled: var(--color-bg-disabled); + --color-radio-group-item-control-border-disabled: var( + --color-border-disabled + ); + --color-radio-group-item-indicator-disabled: var(--color-fg-disabled); + --color-radio-group-item-fg-disabled: var(--color-radio-group-fg-disabled); + --color-radio-group-item-description-disabled: var( + --color-radio-group-fg-disabled + ); + + /* === SPACING === */ + --gap-radio-group-root-sm: var(--spacing-100); + --gap-radio-group-root-md: var(--spacing-150); + --gap-radio-group-root-lg: var(--spacing-200); + + --gap-radio-group-items-vertical-sm: var(--spacing-100); + --gap-radio-group-items-vertical-md: var(--spacing-150); + --gap-radio-group-items-vertical-lg: var(--spacing-200); + + --gap-radio-group-items-horizontal-sm: var(--spacing-150); + --gap-radio-group-items-horizontal-md: var(--spacing-250); + --gap-radio-group-items-horizontal-lg: var(--spacing-300); + + --gap-radio-group-item-sm: var(--spacing-100); + --gap-radio-group-item-md: var(--spacing-150); + --gap-radio-group-item-lg: var(--spacing-200); + + --gap-radio-group-item-content-sm: var(--spacing-50); + --gap-radio-group-item-content-md: var(--spacing-100); + --gap-radio-group-item-content-lg: var(--spacing-100); + + --spacing-radio-group-control-sm: 0.875rem; + --spacing-radio-group-control-md: 1rem; + --spacing-radio-group-control-lg: 1.125rem; + --spacing-radio-group-indicator-sm: 0.5rem; + --spacing-radio-group-indicator-md: 0.625rem; + --spacing-radio-group-indicator-lg: 0.75rem; + + /* === TYPOGRAPHY === */ + --text-radio-group-item-sm: var(--text-sm); + --text-radio-group-item-md: var(--text-md); + --text-radio-group-item-lg: var(--text-lg); + --text-radio-group-item-description-sm: var(--text-sm); + --text-radio-group-item-description-md: var(--text-sm); + --text-radio-group-item-description-lg: var(--text-md); + + /* === BORDERS & RADIUS === */ + --border-width-radio-group: var(--border-width-sm); + --radius-radio-group-control: var(--radius-full); + + /* === SHADOWS === */ + /* No shadows. */ + + /* === FOCUS RINGS === */ + --color-radio-group-ring: var(--color-ring); +} + + +@utility token-icon-radio-group-checked { + @apply icon-[mdi--circle]; +} diff --git a/libs/ui/src/types/zag.ts b/libs/ui/src/types/zag.ts index 3cee69b9c..34456453b 100644 --- a/libs/ui/src/types/zag.ts +++ b/libs/ui/src/types/zag.ts @@ -7,6 +7,7 @@ export type * as Menu from "@zag-js/menu" export type * as NumberInput from "@zag-js/number-input" export type * as Pagination from "@zag-js/pagination" export type * as Popover from "@zag-js/popover" +export type * as RadioGroup from "@zag-js/radio-group" export type * as RatingGroup from "@zag-js/rating-group" export type * as Select from "@zag-js/select" export type * as Slider from "@zag-js/slider" diff --git a/libs/ui/stories/molecules/radio-group.stories.tsx b/libs/ui/stories/molecules/radio-group.stories.tsx new file mode 100644 index 000000000..a8ee98985 --- /dev/null +++ b/libs/ui/stories/molecules/radio-group.stories.tsx @@ -0,0 +1,339 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { useState } from "react" +import { fn } from "storybook/test" +import { VariantContainer, VariantGroup } from "../../.storybook/decorator" +import { Button } from "../../src/atoms/button" +import { + RadioGroup, + type RadioGroupProps, +} from "../../src/molecules/radio-group" + +type RadioOption = { + value: string + label: string + description?: string + disabled?: boolean +} + +const shippingOptions: RadioOption[] = [ + { + value: "standard", + label: "Standard shipping", + description: "Delivers in 3-5 business days.", + }, + { + value: "express", + label: "Express shipping", + description: "Delivers next business day.", + }, + { + value: "pickup", + label: "Store pickup", + description: "Ready in 2 hours.", + disabled: true, + }, +] + +const planOptions: RadioOption[] = [ + { + value: "starter", + label: "Starter", + description: "Best for new projects and quick prototypes.", + }, + { + value: "growth", + label: "Growth", + description: "Adds team features and faster support.", + }, + { + value: "scale", + label: "Scale", + description: "Priority onboarding and dedicated success manager.", + }, +] + +type BasicRadioGroupProps = Omit & { + label?: string + statusText?: string +} + +function BasicRadioGroup({ + label = "Shipping method", + statusText = "Choose one delivery option for the order.", + ...args +}: BasicRadioGroupProps) { + return ( + + {label} + + {shippingOptions.map((option) => ( + + + + + {option.label} + + {option.description && ( + + {option.description} + + )} + + ))} + + {statusText} + + ) +} + +const meta: Meta = { + title: "Molecules/RadioGroup", + component: RadioGroup, + parameters: { + layout: "centered", + docs: { + description: { + component: + "A compound radio-group component built with Zag.js. It supports controlled/uncontrolled state, form submission, validation, visual variants, horizontal or vertical layouts, and rich item content.", + }, + }, + }, + tags: ["autodocs"], + argTypes: { + size: { + control: { type: "select" }, + options: ["sm", "md", "lg"], + description: "Size of the radio group content.", + table: { defaultValue: { summary: "md" } }, + }, + variant: { + control: { type: "inline-radio" }, + options: ["outline", "subtle", "solid"], + description: "Visual treatment for the radio control.", + table: { defaultValue: { summary: "outline" } }, + }, + orientation: { + control: { type: "inline-radio" }, + options: ["vertical", "horizontal"], + description: "Layout orientation for radio items.", + table: { defaultValue: { summary: "vertical" } }, + }, + validateStatus: { + control: { type: "select" }, + options: ["default", "error", "success", "warning"], + description: "Validation state shown in helper text and accessibility.", + table: { defaultValue: { summary: "default" } }, + }, + disabled: { + control: "boolean", + description: "Disable the entire radio group.", + table: { defaultValue: { summary: "false" } }, + }, + readOnly: { + control: "boolean", + description: "Make the radio group read-only.", + table: { defaultValue: { summary: "false" } }, + }, + required: { + control: "boolean", + description: "Mark the field as required.", + table: { defaultValue: { summary: "false" } }, + }, + defaultValue: { + control: { type: "select" }, + options: ["standard", "express", "pickup", null], + description: "Initial selected value for uncontrolled usage.", + }, + onValueChange: { + description: "Called when the selected value changes.", + }, + }, + args: { + size: "md", + variant: "outline", + orientation: "vertical", + validateStatus: "default", + disabled: false, + readOnly: false, + required: false, + defaultValue: "standard", + onValueChange: fn(), + }, +} + +export default meta + +type Story = StoryObj + +export const Playground: Story = { + render: (args) => , +} + +export const Variants: Story = { + render: () => ( + + + + + + + + ), +} + +export const Sizes: Story = { + render: () => ( + + + + + + + + ), +} + +export const States: Story = { + render: () => ( + + + + + + + + + + + + ), +} + +export const Orientations: Story = { + render: () => ( + + + + + + + + + ), +} + +export const Controlled: Story = { + render: () => { + const [value, setValue] = useState("growth") + + return ( +
+ + Plan selection + + {planOptions.map((option) => ( + + + + + {option.label} + + {option.description && ( + + {option.description} + + )} + + ))} + + + Selection syncs with external state. + + + +
+ + + +
+ +
+ Current value: {value ?? "none"} +
+
+ ) + }, +} + +export const RichContent: Story = { + render: () => ( + + Choose a plan + + {planOptions.map((option) => ( + + + + +
+ {option.label} + + {option.value === "growth" ? "Popular" : "Available"} + +
+
+ {option.description && ( + + {option.description} + + )} +
+ ))} +
+ + Compound API lets you add metadata next to each option. + +
+ ), +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb207d606..23405c3cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -368,6 +368,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 + apps/zane-operator: {} + libs/analytics: dependencies: next: @@ -480,6 +482,9 @@ importers: '@zag-js/popover': specifier: ^1.31.1 version: 1.31.1 + '@zag-js/radio-group': + specifier: ^1.31.1 + version: 1.31.1 '@zag-js/rating-group': specifier: ^1.31.1 version: 1.31.1 @@ -7539,6 +7544,9 @@ packages: '@zag-js/popper@1.31.1': resolution: {integrity: sha512-wLXcEqzn9MK1rGbsgnDH26o5ZWqR4oeb6ZepKKy0gcuJl/1S5/dr1VBvxJNMZlf9d6etvYklG5LRnIVkXCbrjA==} + '@zag-js/radio-group@1.31.1': + resolution: {integrity: sha512-OfKIdEtSG0EuHM+cFVqcR+04yzZmcDRgG3j0QhoJsyS1my63ZHbwC2HNAtfPFh4U4sJx9yUexwSzPGZ6pOzIdw==} + '@zag-js/rating-group@1.31.1': resolution: {integrity: sha512-BkQUglKm4a+KXYPACYvIvBJSuEyzV0YQqjjiucwJ5UiOlK72C66VBvyGN+DqJRDnkU1K5azt6E1Ja5ANk3fgsg==} @@ -22448,6 +22456,15 @@ snapshots: '@zag-js/dom-query': 1.31.1 '@zag-js/utils': 1.31.1 + '@zag-js/radio-group@1.31.1': + dependencies: + '@zag-js/anatomy': 1.31.1 + '@zag-js/core': 1.31.1 + '@zag-js/dom-query': 1.31.1 + '@zag-js/focus-visible': 1.31.1 + '@zag-js/types': 1.31.1 + '@zag-js/utils': 1.31.1 + '@zag-js/rating-group@1.31.1': dependencies: '@zag-js/anatomy': 1.31.1