From 728a311f2d18bff498aee25b8821dcfc95c98fcd Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Thu, 23 Jul 2026 10:50:11 -0400 Subject: [PATCH 1/5] wip --- .changeset/bordered-choice-groups.md | 5 + .../src/components/demos/CheckboxDemo.tsx | 117 +++++++++- .../src/components/demos/RadioDemo.tsx | 102 +++++++++ .../src/components/demos/SidebarDemo.tsx | 55 +++++ .../src/pages/components/checkbox.mdx | 14 ++ .../src/pages/components/radio.mdx | 14 ++ .../src/components/checkbox/checkbox.test.tsx | 81 +++++++ .../kumo/src/components/checkbox/checkbox.tsx | 199 +++++++++++------- .../kumo/src/components/checkbox/index.ts | 2 + .../kumo/src/components/radio/radio.test.tsx | 60 ++++++ 10 files changed, 571 insertions(+), 78 deletions(-) create mode 100644 .changeset/bordered-choice-groups.md create mode 100644 packages/kumo/src/components/checkbox/checkbox.test.tsx diff --git a/.changeset/bordered-choice-groups.md b/.changeset/bordered-choice-groups.md new file mode 100644 index 0000000000..784a40a496 --- /dev/null +++ b/.changeset/bordered-choice-groups.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/kumo": minor +--- + +Add bordered appearances and horizontal orientation support to radio and checkbox groups. diff --git a/packages/kumo-docs-astro/src/components/demos/CheckboxDemo.tsx b/packages/kumo-docs-astro/src/components/demos/CheckboxDemo.tsx index b50844c2d6..e9414f0eb1 100644 --- a/packages/kumo-docs-astro/src/components/demos/CheckboxDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/CheckboxDemo.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; -import { Checkbox } from "@cloudflare/kumo"; +import { useState } from 'react'; +import { Badge, Checkbox } from '@cloudflare/kumo'; +import { ChatCircleTextIcon, EnvelopeIcon } from '@phosphor-icons/react'; export function CheckboxBasicDemo() { const [checked, setChecked] = useState(false); @@ -62,7 +63,7 @@ export function CheckboxErrorDemo() { } export function CheckboxGroupDemo() { - const [preferences, setPreferences] = useState(["email"]); + const [preferences, setPreferences] = useState(['email']); return ( (['email']); + const [alertCategories, setAlertCategories] = useState([ + 'security', + 'performance' + ]); + const [exportContents, setExportContents] = useState([ + 'configuration', + 'analytics' + ]); + const [permissions, setPermissions] = useState(['read', 'edit']); + + return ( +
+ + + + Email + + } + /> + + + SMS + + } + /> + + + + + + Reliability + Beta + + } + /> + + + + + + + + + + + + + + +
+ ); +} + /** Shows Checkbox.Legend with sr-only to visually hide the legend while keeping it accessible, useful when a parent Field already provides a visible label */ export function CheckboxLegendSrOnlyDemo() { - const [preferences, setPreferences] = useState(["email"]); + const [preferences, setPreferences] = useState(['email']); return ( @@ -95,7 +202,7 @@ export function CheckboxLegendSrOnlyDemo() { /** Shows Checkbox.Legend with custom styling for full control over legend presentation */ export function CheckboxLegendCustomDemo() { - const [preferences, setPreferences] = useState(["email"]); + const [preferences, setPreferences] = useState(['email']); return ( diff --git a/packages/kumo-docs-astro/src/components/demos/RadioDemo.tsx b/packages/kumo-docs-astro/src/components/demos/RadioDemo.tsx index d6c0c4f0d3..172a48a568 100644 --- a/packages/kumo-docs-astro/src/components/demos/RadioDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/RadioDemo.tsx @@ -1,5 +1,10 @@ import { useState } from "react"; import { Badge, Radio } from "@cloudflare/kumo"; +import { + GitBranchIcon, + GlobeIcon, + ShieldCheckIcon, +} from "@phosphor-icons/react"; /** Shows a basic controlled radio group */ export function RadioBasicDemo() { @@ -46,6 +51,103 @@ export function RadioHorizontalDemo() { ); } +/** Shows bordered radio groups in horizontal and vertical layouts */ +export function RadioBorderedDemo() { + const [scope, setScope] = useState("all"); + const [environment, setEnvironment] = useState("production"); + const [routingMode, setRoutingMode] = useState("smart"); + const [securityLevel, setSecurityLevel] = useState("balanced"); + + return ( +
+ + + + All traffic + + } + value="all" + /> + + + Previews only + + } + value="previews" + /> + + + + + + + + + + + + + + + + + Balanced (recommended) + Default + + } + value="balanced" + /> + + + +
+ ); +} + /** Shows a radio group with helper description text */ export function RadioDescriptionDemo() { const [value, setValue] = useState("standard"); diff --git a/packages/kumo-docs-astro/src/components/demos/SidebarDemo.tsx b/packages/kumo-docs-astro/src/components/demos/SidebarDemo.tsx index 18ff96bb0a..39635652ba 100644 --- a/packages/kumo-docs-astro/src/components/demos/SidebarDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/SidebarDemo.tsx @@ -304,6 +304,61 @@ export function SidebarLoadingDemo() { ); } +function SidebarLoadingSkeleton() { + return ( + <> + +
+
+
+
+ + + {["72%", "58%"].map((labelWidth, groupIndex) => ( + +
+
+ {["68%", "82%", "55%"].map((width, rowIndex) => ( +
+
+
+
+ ))} +
+ + ))} + + + + + + ); +} + +/** Concept loading state that mirrors sidebar groups in expanded and collapsed modes. */ +export function SidebarLoadingDemo() { + return ( + + + + + + + +

Toggle to preview both loading states

+
+
+
+ ); +} + // --------------------------------------------------------------------------- // 3. Resizable — drag handle with auto-collapse // --------------------------------------------------------------------------- diff --git a/packages/kumo-docs-astro/src/pages/components/checkbox.mdx b/packages/kumo-docs-astro/src/pages/components/checkbox.mdx index a0065e323f..cb8d33f849 100644 --- a/packages/kumo-docs-astro/src/pages/components/checkbox.mdx +++ b/packages/kumo-docs-astro/src/pages/components/checkbox.mdx @@ -19,6 +19,7 @@ import { CheckboxDisabledDemo, CheckboxErrorDemo, CheckboxGroupDemo, + CheckboxBorderedGroupDemo, CheckboxGroupErrorDemo, CheckboxLegendSrOnlyDemo, CheckboxLegendCustomDemo, @@ -128,6 +129,19 @@ export default function Example() { +### Bordered Checkbox Group + +

+ Use `appearance="bordered"` to place checkbox options in one contiguous + surface with shared dividers. Set `orientation="horizontal"` for a + side-by-side layout; groups are vertical by default. Bordered groups place + controls at the end unless `controlFirst` is set. The examples below also + cover long wrapping text, disabled options, and larger option sets. +

+ + + + ### Checkbox Group with Error

diff --git a/packages/kumo-docs-astro/src/pages/components/radio.mdx b/packages/kumo-docs-astro/src/pages/components/radio.mdx index 2c59c705dd..b809de29cb 100644 --- a/packages/kumo-docs-astro/src/pages/components/radio.mdx +++ b/packages/kumo-docs-astro/src/pages/components/radio.mdx @@ -13,6 +13,7 @@ import { RadioBasicDemo, RadioDefaultDemo, RadioHorizontalDemo, + RadioBorderedDemo, RadioDescriptionDemo, RadioErrorDemo, RadioDisabledDemo, @@ -101,6 +102,19 @@ export default function Example() { +### Bordered Group + +

+ Use `appearance="bordered"` to place options in one contiguous surface with + shared dividers. It supports both vertical and horizontal orientations and + places controls at the end by default. Set `controlPosition="start"` to place + controls before their labels. The examples below also cover rich labels, long + wrapping text, disabled options, and larger option sets. +

+ + + + ### With Description

Add helper text below the radio items using the `description` prop.

diff --git a/packages/kumo/src/components/checkbox/checkbox.test.tsx b/packages/kumo/src/components/checkbox/checkbox.test.tsx new file mode 100644 index 0000000000..3cb9ae0d86 --- /dev/null +++ b/packages/kumo/src/components/checkbox/checkbox.test.tsx @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { Checkbox } from './checkbox'; + +describe('Checkbox.Group', () => { + it.each([ + ['vertical', 'border-t'], + ['horizontal', 'border-l'] + ] as const)( + 'renders a bordered %s group with shared dividers', + (orientation, dividerClass) => { + const { container } = render( + + + + + ); + + const group = container.querySelector('[data-kumo-part="group-items"]'); + const item = container.querySelector('[data-kumo-part="item-label"]'); + + expect(group?.className).toContain('rounded-lg'); + expect(group?.className).toContain(dividerClass); + expect(item?.className).toContain('flex-1'); + expect(item?.className).toContain('flex-row-reverse'); + expect(item?.className).toContain('bg-kumo-elevated'); + expect( + item?.querySelector('[data-kumo-part="item-content"]')?.className + ).toContain('leading-5'); + } + ); + + it('preserves control-first layout when explicitly requested', () => { + const { container } = render( + + + + ); + + expect( + container.querySelector('[data-kumo-part="item-label"]')?.className + ).not.toContain('flex-row-reverse'); + }); + + it('renders rich label content', () => { + const { getByText } = render( + + + Security Recommended + + } + value="security" + /> + + ); + + expect(getByText('Recommended').tagName).toBe('STRONG'); + }); + + it('dims only content for disabled bordered items', () => { + const { container } = render( + + + + ); + + const itemClassName = container.querySelector( + '[data-kumo-part="item-label"]' + )?.className; + + expect(itemClassName).toContain('[&>*]:opacity-50'); + expect(itemClassName?.split(' ')).not.toContain('opacity-50'); + }); +}); diff --git a/packages/kumo/src/components/checkbox/checkbox.tsx b/packages/kumo/src/components/checkbox/checkbox.tsx index 2aa42fa6ce..c78a31bf35 100644 --- a/packages/kumo/src/components/checkbox/checkbox.tsx +++ b/packages/kumo/src/components/checkbox/checkbox.tsx @@ -1,16 +1,16 @@ -import { forwardRef, createContext, useContext, type ReactNode } from "react"; -import { CheckIcon, MinusIcon } from "@phosphor-icons/react"; -import { cn } from "../../utils/cn"; -import { resolveVariant } from "../../utils/resolve-variant"; -import { Label } from "../label"; -import { Fieldset } from "@base-ui/react/fieldset"; -import { Field as FieldBase } from "@base-ui/react/field"; -import { CheckboxGroup as BaseCheckboxGroup } from "@base-ui/react/checkbox-group"; -import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox"; +import { forwardRef, createContext, useContext, type ReactNode } from 'react'; +import { CheckIcon, MinusIcon } from '@phosphor-icons/react'; +import { cn } from '../../utils/cn'; +import { resolveVariant } from '../../utils/resolve-variant'; +import { Label } from '../label'; +import { Fieldset } from '@base-ui/react/fieldset'; +import { Field as FieldBase } from '@base-ui/react/field'; +import { CheckboxGroup as BaseCheckboxGroup } from '@base-ui/react/checkbox-group'; +import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox'; /** Event details passed to onCheckedChange callback. Re-exported from Base UI. */ export type CheckboxChangeEventDetails = Parameters< - NonNullable + NonNullable >[1]; /** Checkbox variant definitions mapping variant names to their Tailwind classes. */ @@ -18,18 +18,18 @@ export const KUMO_CHECKBOX_VARIANTS = { variant: { default: { classes: - "[&:focus-within>span]:ring-kumo-focus [&:hover>span]:ring-kumo-hairline", - description: "Default checkbox appearance", + '[&:focus-within>span]:ring-kumo-focus [&:hover>span]:ring-kumo-hairline', + description: 'Default checkbox appearance' }, error: { - classes: "[&>span]:ring-kumo-danger", - description: "Error state for validation failures", - }, - }, + classes: '[&>span]:ring-kumo-danger', + description: 'Error state for validation failures' + } + } } as const; export const KUMO_CHECKBOX_DEFAULT_VARIANTS = { - variant: "default", + variant: 'default' } as const; // Derived types from KUMO_CHECKBOX_VARIANTS @@ -46,23 +46,33 @@ export interface KumoCheckboxVariantsProps { } export function checkboxVariants({ - variant = KUMO_CHECKBOX_DEFAULT_VARIANTS.variant, + variant = KUMO_CHECKBOX_DEFAULT_VARIANTS.variant }: KumoCheckboxVariantsProps = {}) { return cn( resolveVariant( KUMO_CHECKBOX_VARIANTS.variant, variant, - KUMO_CHECKBOX_DEFAULT_VARIANTS.variant, - ).classes, + KUMO_CHECKBOX_DEFAULT_VARIANTS.variant + ).classes ); } // Legacy type alias for backwards compatibility export type CheckboxVariant = KumoCheckboxVariant; -// Context for passing controlFirst from Group to Items -const CheckboxGroupContext = createContext<{ controlFirst: boolean }>({ +/** Visual treatment for items within a checkbox group. */ +export type CheckboxGroupAppearance = 'default' | 'bordered'; + +/** Layout direction for items within a checkbox group. */ +export type CheckboxGroupOrientation = 'vertical' | 'horizontal'; + +// Context for passing group layout options to Items. +const CheckboxGroupContext = createContext<{ + controlFirst: boolean; + appearance: CheckboxGroupAppearance; +}>({ controlFirst: true, + appearance: 'default' }); /** @@ -125,7 +135,7 @@ export type CheckboxProps = { /** Whether the checkbox is disabled */ disabled?: boolean; /** Callback when the checked state changes */ - onCheckedChange?: BaseCheckbox.Root.Props["onCheckedChange"]; + onCheckedChange?: BaseCheckbox.Root.Props['onCheckedChange']; /** Name for form submission */ name?: string; /** Whether the field is required */ @@ -133,9 +143,9 @@ export type CheckboxProps = { /** Additional class name */ className?: string; /** Accessible label when no visible label is provided */ - "aria-label"?: string; + 'aria-label'?: string; /** ID of element that labels this checkbox */ - "aria-labelledby"?: string; + 'aria-labelledby'?: string; }; /** @@ -196,7 +206,11 @@ export interface CheckboxGroupProps { allValues?: string[]; /** Whether all checkboxes in the group are disabled */ disabled?: boolean; - /** When true (default), checkbox appears before label. When false, label appears before checkbox. */ + /** Layout direction of the checkbox items. */ + orientation?: CheckboxGroupOrientation; + /** Visual treatment applied to the group. */ + appearance?: CheckboxGroupAppearance; + /** When true, checkbox appears before label. When false, label appears before checkbox. Defaults to true for default appearance and false for bordered appearance. */ controlFirst?: boolean; /** Additional CSS classes */ className?: string; @@ -208,8 +222,8 @@ export interface CheckboxGroupProps { export type CheckboxItemProps = { /** Visual variant: "default" or "error" for validation failures */ variant?: CheckboxVariant; - /** Label text displayed next to checkbox */ - label: string; + /** Label content displayed next to checkbox */ + label: ReactNode; /** Value of the checkbox (required when used in Checkbox.Group) */ value?: string; /** Additional CSS classes for the label wrapper */ @@ -218,7 +232,7 @@ export type CheckboxItemProps = { indeterminate?: boolean; disabled?: boolean; /** Callback when the checked state changes */ - onCheckedChange?: BaseCheckbox.Root.Props["onCheckedChange"]; + onCheckedChange?: BaseCheckbox.Root.Props['onCheckedChange']; name?: string; }; @@ -230,7 +244,7 @@ const CheckboxBase = forwardRef( checked, indeterminate, disabled, - variant = "default", + variant = 'default', label, labelTooltip, controlFirst = true, @@ -239,21 +253,21 @@ const CheckboxBase = forwardRef( name, ...props }, - ref, + ref ) => { // A11y enforcement: warn in dev if no accessible name provided - if (process.env.NODE_ENV !== "production") { + if (process.env.NODE_ENV !== 'production') { const hasLabel = Boolean(label); - const hasAriaLabel = Boolean(props["aria-label"]); - const hasAriaLabelledBy = Boolean(props["aria-labelledby"]); + const hasAriaLabel = Boolean(props['aria-label']); + const hasAriaLabelledBy = Boolean(props['aria-labelledby']); if (!hasLabel && !hasAriaLabel && !hasAriaLabelledBy) { console.warn( - "[Kumo Checkbox]: Checkbox must have an accessible name. Provide either:\n" + + '[Kumo Checkbox]: Checkbox must have an accessible name. Provide either:\n' + " - label prop: \n" + " - aria-label: \n" + - " - aria-labelledby for custom label association\n" + - " Note: When used inside Checkbox.Group, label is optional", + ' - aria-labelledby for custom label association\n' + + ' Note: When used inside Checkbox.Group, label is optional' ); } } @@ -268,14 +282,14 @@ const CheckboxBase = forwardRef( disabled={disabled} onCheckedChange={onCheckedChange} className={cn( - "relative flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-0 bg-kumo-base ring after:absolute after:-inset-x-3 after:-inset-y-2 focus:outline-none", - label && "mt-0.5", - variant === "error" ? "ring-kumo-danger" : "ring-kumo-hairline", + 'relative flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-0 bg-kumo-base ring focus:outline-none after:absolute after:-inset-x-3 after:-inset-y-2', + label && 'mt-0.5', + variant === 'error' ? 'ring-kumo-danger' : 'ring-kumo-hairline', !disabled && - "hover:ring-kumo-hairline focus:ring-2 focus:ring-kumo-focus focus-visible:ring-2 focus-visible:ring-kumo-brand", - "data-[checked]:bg-kumo-contrast data-[checked]:ring-kumo-contrast data-[indeterminate]:bg-kumo-contrast data-[indeterminate]:ring-kumo-contrast", - disabled && "cursor-not-allowed opacity-50", - className, + 'hover:ring-kumo-hairline focus:ring-kumo-focus focus:ring-2 focus-visible:ring-2 focus-visible:ring-kumo-brand', + 'data-[checked]:bg-kumo-contrast data-[checked]:ring-kumo-contrast data-[indeterminate]:bg-kumo-contrast data-[indeterminate]:ring-kumo-contrast', + disabled && 'cursor-not-allowed opacity-50', + className )} {...props} > @@ -306,9 +320,9 @@ const CheckboxBase = forwardRef( {checkboxControl} @@ -322,10 +336,10 @@ const CheckboxBase = forwardRef( ); - }, + } ); -CheckboxBase.displayName = "Checkbox"; +CheckboxBase.displayName = 'Checkbox'; // Checkbox.Item for use within Checkbox.Group const CheckboxItem = forwardRef( @@ -335,27 +349,35 @@ const CheckboxItem = forwardRef( checked, indeterminate, disabled, - variant = "default", + variant = 'default', label, value, onCheckedChange, - name, + name }, - ref, + ref ) => { - const { controlFirst } = useContext(CheckboxGroupContext); + const { controlFirst, appearance } = useContext(CheckboxGroupContext); + const isBordered = appearance === 'bordered'; return ( ); - }, + } ); -CheckboxItem.displayName = "Checkbox.Item"; +CheckboxItem.displayName = 'Checkbox.Item'; // Checkbox.Legend — composable legend sub-component for Checkbox.Group function CheckboxLegend({ children, className }: CheckboxLegendProps) { return ( {children} ); } -CheckboxLegend.displayName = "Checkbox.Legend"; +CheckboxLegend.displayName = 'Checkbox.Legend'; // Checkbox.Group with built-in Fieldset and CheckboxGroup function CheckboxGroup({ @@ -422,11 +452,17 @@ function CheckboxGroup({ onValueChange, allValues, disabled, - controlFirst = true, - className, + orientation = 'vertical', + appearance = 'default', + controlFirst, + className }: CheckboxGroupProps) { + const effectiveControlFirst = controlFirst ?? appearance === 'default'; + return ( - + - + {legend && ( {legend} )} -
{children}
+
[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-t [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-kumo-line' + : 'flex flex-row [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-l [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-kumo-line' + ) + : orientation === 'vertical' + ? 'flex flex-col gap-2' + : 'flex flex-row flex-wrap gap-2' + )} + > + {children} +
{error &&

{error}

} {description && (

{description}

@@ -455,7 +508,7 @@ function CheckboxGroup({ export const Checkbox = Object.assign(CheckboxBase, { Item: CheckboxItem, Group: CheckboxGroup, - Legend: CheckboxLegend, + Legend: CheckboxLegend }); -Checkbox.displayName = "Checkbox"; +Checkbox.displayName = 'Checkbox'; diff --git a/packages/kumo/src/components/checkbox/index.ts b/packages/kumo/src/components/checkbox/index.ts index 0fc6d2f42d..c076819eb2 100644 --- a/packages/kumo/src/components/checkbox/index.ts +++ b/packages/kumo/src/components/checkbox/index.ts @@ -6,6 +6,8 @@ export { type CheckboxProps, type CheckboxLegendProps, type CheckboxGroupProps, + type CheckboxGroupAppearance, + type CheckboxGroupOrientation, type CheckboxItemProps, type KumoCheckboxVariant, type CheckboxVariant, diff --git a/packages/kumo/src/components/radio/radio.test.tsx b/packages/kumo/src/components/radio/radio.test.tsx index f85f6952a7..184145621d 100644 --- a/packages/kumo/src/components/radio/radio.test.tsx +++ b/packages/kumo/src/components/radio/radio.test.tsx @@ -101,6 +101,66 @@ describe("Radio", () => { it("exports KUMO_RADIO_VARIANTS with appearance axis", () => { expect(KUMO_RADIO_VARIANTS.appearance.default).toBeDefined(); expect(KUMO_RADIO_VARIANTS.appearance.card).toBeDefined(); + expect(KUMO_RADIO_VARIANTS.appearance.bordered).toBeDefined(); expect(KUMO_RADIO_DEFAULT_VARIANTS.appearance).toBe("default"); }); + + it.each([ + ["vertical", "border-t"], + ["horizontal", "border-l"], + ] as const)( + "renders a bordered %s group with shared dividers", + (orientation, dividerClass) => { + const { container } = render( + + + + , + ); + + const group = container.querySelector('[data-kumo-part="group-items"]'); + const item = container.querySelector('[data-kumo-part="item-label"]'); + + expect(group?.className).toContain("rounded-lg"); + expect(group?.className).toContain(dividerClass); + expect(item?.className).toContain("flex-1"); + expect(item?.className).toContain("flex-row-reverse"); + expect(item?.className).toContain("bg-kumo-elevated"); + expect( + item?.querySelector('[data-kumo-part="item-content"]')?.className, + ).toContain("leading-5"); + }, + ); + + it("supports controlPosition='start' on bordered appearance", () => { + const { container } = render( + + + , + ); + + expect( + container.querySelector('[data-kumo-part="item-label"]')?.className, + ).not.toContain("flex-row-reverse"); + }); + + it("dims only content for disabled bordered items", () => { + const { container } = render( + + + , + ); + + const itemClassName = container.querySelector( + '[data-kumo-part="item-label"]', + )?.className; + + expect(itemClassName).toContain("[&>*]:opacity-50"); + expect(itemClassName?.split(" ")).not.toContain("opacity-50"); + }); }); From d7fd1a1a4aa32959b3b344ba3d2043c659521b82 Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Fri, 24 Jul 2026 13:08:17 -0400 Subject: [PATCH 2/5] feat(checkbox,radio): add bordered group appearance and horizontal orientation --- .../src/components/demos/CheckboxDemo.tsx | 24 +-- .../src/components/checkbox/checkbox.test.tsx | 56 +++--- .../kumo/src/components/checkbox/checkbox.tsx | 166 +++++++++--------- packages/kumo/src/components/radio/radio.tsx | 12 +- 4 files changed, 134 insertions(+), 124 deletions(-) diff --git a/packages/kumo-docs-astro/src/components/demos/CheckboxDemo.tsx b/packages/kumo-docs-astro/src/components/demos/CheckboxDemo.tsx index e9414f0eb1..c8227d81a6 100644 --- a/packages/kumo-docs-astro/src/components/demos/CheckboxDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/CheckboxDemo.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react'; -import { Badge, Checkbox } from '@cloudflare/kumo'; -import { ChatCircleTextIcon, EnvelopeIcon } from '@phosphor-icons/react'; +import { useState } from "react"; +import { Badge, Checkbox } from "@cloudflare/kumo"; +import { ChatCircleTextIcon, EnvelopeIcon } from "@phosphor-icons/react"; export function CheckboxBasicDemo() { const [checked, setChecked] = useState(false); @@ -63,7 +63,7 @@ export function CheckboxErrorDemo() { } export function CheckboxGroupDemo() { - const [preferences, setPreferences] = useState(['email']); + const [preferences, setPreferences] = useState(["email"]); return ( (['email']); + const [notifications, setNotifications] = useState(["email"]); const [alertCategories, setAlertCategories] = useState([ - 'security', - 'performance' + "security", + "performance", ]); const [exportContents, setExportContents] = useState([ - 'configuration', - 'analytics' + "configuration", + "analytics", ]); - const [permissions, setPermissions] = useState(['read', 'edit']); + const [permissions, setPermissions] = useState(["read", "edit"]); return (
@@ -187,7 +187,7 @@ export function CheckboxBorderedGroupDemo() { /** Shows Checkbox.Legend with sr-only to visually hide the legend while keeping it accessible, useful when a parent Field already provides a visible label */ export function CheckboxLegendSrOnlyDemo() { - const [preferences, setPreferences] = useState(['email']); + const [preferences, setPreferences] = useState(["email"]); return ( @@ -202,7 +202,7 @@ export function CheckboxLegendSrOnlyDemo() { /** Shows Checkbox.Legend with custom styling for full control over legend presentation */ export function CheckboxLegendCustomDemo() { - const [preferences, setPreferences] = useState(['email']); + const [preferences, setPreferences] = useState(["email"]); return ( diff --git a/packages/kumo/src/components/checkbox/checkbox.test.tsx b/packages/kumo/src/components/checkbox/checkbox.test.tsx index 3cb9ae0d86..a2eb4f0825 100644 --- a/packages/kumo/src/components/checkbox/checkbox.test.tsx +++ b/packages/kumo/src/components/checkbox/checkbox.test.tsx @@ -1,53 +1,53 @@ -import { describe, expect, it } from 'vitest'; -import { render } from '@testing-library/react'; -import { Checkbox } from './checkbox'; +import { describe, expect, it } from "vite-plus/test"; +import { render } from "@testing-library/react"; +import { Checkbox } from "./checkbox"; -describe('Checkbox.Group', () => { +describe("Checkbox.Group", () => { it.each([ - ['vertical', 'border-t'], - ['horizontal', 'border-l'] + ["vertical", "border-t"], + ["horizontal", "border-l"], ] as const)( - 'renders a bordered %s group with shared dividers', + "renders a bordered %s group with shared dividers", (orientation, dividerClass) => { const { container } = render( - + , ); const group = container.querySelector('[data-kumo-part="group-items"]'); const item = container.querySelector('[data-kumo-part="item-label"]'); - expect(group?.className).toContain('rounded-lg'); + expect(group?.className).toContain("rounded-lg"); expect(group?.className).toContain(dividerClass); - expect(item?.className).toContain('flex-1'); - expect(item?.className).toContain('flex-row-reverse'); - expect(item?.className).toContain('bg-kumo-elevated'); + expect(item?.className).toContain("flex-1"); + expect(item?.className).toContain("flex-row-reverse"); + expect(item?.className).toContain("bg-kumo-elevated"); expect( - item?.querySelector('[data-kumo-part="item-content"]')?.className - ).toContain('leading-5'); - } + item?.querySelector('[data-kumo-part="item-content"]')?.className, + ).toContain("leading-5"); + }, ); - it('preserves control-first layout when explicitly requested', () => { + it("preserves control-first layout when explicitly requested", () => { const { container } = render( - + , ); expect( - container.querySelector('[data-kumo-part="item-label"]')?.className - ).not.toContain('flex-row-reverse'); + container.querySelector('[data-kumo-part="item-label"]')?.className, + ).not.toContain("flex-row-reverse"); }); - it('renders rich label content', () => { + it("renders rich label content", () => { const { getByText } = render( { } value="security" /> - + , ); - expect(getByText('Recommended').tagName).toBe('STRONG'); + expect(getByText("Recommended").tagName).toBe("STRONG"); }); - it('dims only content for disabled bordered items', () => { + it("dims only content for disabled bordered items", () => { const { container } = render( - + , ); const itemClassName = container.querySelector( - '[data-kumo-part="item-label"]' + '[data-kumo-part="item-label"]', )?.className; - expect(itemClassName).toContain('[&>*]:opacity-50'); - expect(itemClassName?.split(' ')).not.toContain('opacity-50'); + expect(itemClassName).toContain("[&>*]:opacity-50"); + expect(itemClassName?.split(" ")).not.toContain("opacity-50"); }); }); diff --git a/packages/kumo/src/components/checkbox/checkbox.tsx b/packages/kumo/src/components/checkbox/checkbox.tsx index c78a31bf35..3b29a97894 100644 --- a/packages/kumo/src/components/checkbox/checkbox.tsx +++ b/packages/kumo/src/components/checkbox/checkbox.tsx @@ -1,16 +1,16 @@ -import { forwardRef, createContext, useContext, type ReactNode } from 'react'; -import { CheckIcon, MinusIcon } from '@phosphor-icons/react'; -import { cn } from '../../utils/cn'; -import { resolveVariant } from '../../utils/resolve-variant'; -import { Label } from '../label'; -import { Fieldset } from '@base-ui/react/fieldset'; -import { Field as FieldBase } from '@base-ui/react/field'; -import { CheckboxGroup as BaseCheckboxGroup } from '@base-ui/react/checkbox-group'; -import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox'; +import { forwardRef, createContext, useContext, type ReactNode } from "react"; +import { CheckIcon, MinusIcon } from "@phosphor-icons/react"; +import { cn } from "../../utils/cn"; +import { resolveVariant } from "../../utils/resolve-variant"; +import { Label } from "../label"; +import { Fieldset } from "@base-ui/react/fieldset"; +import { Field as FieldBase } from "@base-ui/react/field"; +import { CheckboxGroup as BaseCheckboxGroup } from "@base-ui/react/checkbox-group"; +import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox"; /** Event details passed to onCheckedChange callback. Re-exported from Base UI. */ export type CheckboxChangeEventDetails = Parameters< - NonNullable + NonNullable >[1]; /** Checkbox variant definitions mapping variant names to their Tailwind classes. */ @@ -18,18 +18,18 @@ export const KUMO_CHECKBOX_VARIANTS = { variant: { default: { classes: - '[&:focus-within>span]:ring-kumo-focus [&:hover>span]:ring-kumo-hairline', - description: 'Default checkbox appearance' + "[&:focus-within>span]:ring-kumo-focus [&:hover>span]:ring-kumo-hairline", + description: "Default checkbox appearance", }, error: { - classes: '[&>span]:ring-kumo-danger', - description: 'Error state for validation failures' - } - } + classes: "[&>span]:ring-kumo-danger", + description: "Error state for validation failures", + }, + }, } as const; export const KUMO_CHECKBOX_DEFAULT_VARIANTS = { - variant: 'default' + variant: "default", } as const; // Derived types from KUMO_CHECKBOX_VARIANTS @@ -46,14 +46,14 @@ export interface KumoCheckboxVariantsProps { } export function checkboxVariants({ - variant = KUMO_CHECKBOX_DEFAULT_VARIANTS.variant + variant = KUMO_CHECKBOX_DEFAULT_VARIANTS.variant, }: KumoCheckboxVariantsProps = {}) { return cn( resolveVariant( KUMO_CHECKBOX_VARIANTS.variant, variant, - KUMO_CHECKBOX_DEFAULT_VARIANTS.variant - ).classes + KUMO_CHECKBOX_DEFAULT_VARIANTS.variant, + ).classes, ); } @@ -61,10 +61,10 @@ export function checkboxVariants({ export type CheckboxVariant = KumoCheckboxVariant; /** Visual treatment for items within a checkbox group. */ -export type CheckboxGroupAppearance = 'default' | 'bordered'; +export type CheckboxGroupAppearance = "default" | "bordered"; /** Layout direction for items within a checkbox group. */ -export type CheckboxGroupOrientation = 'vertical' | 'horizontal'; +export type CheckboxGroupOrientation = "vertical" | "horizontal"; // Context for passing group layout options to Items. const CheckboxGroupContext = createContext<{ @@ -72,7 +72,7 @@ const CheckboxGroupContext = createContext<{ appearance: CheckboxGroupAppearance; }>({ controlFirst: true, - appearance: 'default' + appearance: "default", }); /** @@ -135,7 +135,7 @@ export type CheckboxProps = { /** Whether the checkbox is disabled */ disabled?: boolean; /** Callback when the checked state changes */ - onCheckedChange?: BaseCheckbox.Root.Props['onCheckedChange']; + onCheckedChange?: BaseCheckbox.Root.Props["onCheckedChange"]; /** Name for form submission */ name?: string; /** Whether the field is required */ @@ -143,9 +143,9 @@ export type CheckboxProps = { /** Additional class name */ className?: string; /** Accessible label when no visible label is provided */ - 'aria-label'?: string; + "aria-label"?: string; /** ID of element that labels this checkbox */ - 'aria-labelledby'?: string; + "aria-labelledby"?: string; }; /** @@ -232,7 +232,7 @@ export type CheckboxItemProps = { indeterminate?: boolean; disabled?: boolean; /** Callback when the checked state changes */ - onCheckedChange?: BaseCheckbox.Root.Props['onCheckedChange']; + onCheckedChange?: BaseCheckbox.Root.Props["onCheckedChange"]; name?: string; }; @@ -244,7 +244,7 @@ const CheckboxBase = forwardRef( checked, indeterminate, disabled, - variant = 'default', + variant = "default", label, labelTooltip, controlFirst = true, @@ -253,21 +253,21 @@ const CheckboxBase = forwardRef( name, ...props }, - ref + ref, ) => { // A11y enforcement: warn in dev if no accessible name provided - if (process.env.NODE_ENV !== 'production') { + if (process.env.NODE_ENV !== "production") { const hasLabel = Boolean(label); - const hasAriaLabel = Boolean(props['aria-label']); - const hasAriaLabelledBy = Boolean(props['aria-labelledby']); + const hasAriaLabel = Boolean(props["aria-label"]); + const hasAriaLabelledBy = Boolean(props["aria-labelledby"]); if (!hasLabel && !hasAriaLabel && !hasAriaLabelledBy) { console.warn( - '[Kumo Checkbox]: Checkbox must have an accessible name. Provide either:\n' + + "[Kumo Checkbox]: Checkbox must have an accessible name. Provide either:\n" + " - label prop: \n" + " - aria-label: \n" + - ' - aria-labelledby for custom label association\n' + - ' Note: When used inside Checkbox.Group, label is optional' + " - aria-labelledby for custom label association\n" + + " Note: When used inside Checkbox.Group, label is optional", ); } } @@ -282,14 +282,14 @@ const CheckboxBase = forwardRef( disabled={disabled} onCheckedChange={onCheckedChange} className={cn( - 'relative flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-0 bg-kumo-base ring focus:outline-none after:absolute after:-inset-x-3 after:-inset-y-2', - label && 'mt-0.5', - variant === 'error' ? 'ring-kumo-danger' : 'ring-kumo-hairline', + "relative flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border-0 bg-kumo-base ring after:absolute after:-inset-x-3 after:-inset-y-2 focus:outline-none", + label && "mt-0.5", + variant === "error" ? "ring-kumo-danger" : "ring-kumo-hairline", !disabled && - 'hover:ring-kumo-hairline focus:ring-kumo-focus focus:ring-2 focus-visible:ring-2 focus-visible:ring-kumo-brand', - 'data-[checked]:bg-kumo-contrast data-[checked]:ring-kumo-contrast data-[indeterminate]:bg-kumo-contrast data-[indeterminate]:ring-kumo-contrast', - disabled && 'cursor-not-allowed opacity-50', - className + "hover:ring-kumo-hairline focus:ring-2 focus:ring-kumo-focus focus-visible:ring-2 focus-visible:ring-kumo-brand", + "data-[checked]:bg-kumo-contrast data-[checked]:ring-kumo-contrast data-[indeterminate]:bg-kumo-contrast data-[indeterminate]:ring-kumo-contrast", + disabled && "cursor-not-allowed opacity-50", + className, )} {...props} > @@ -320,9 +320,9 @@ const CheckboxBase = forwardRef( {checkboxControl} @@ -336,10 +336,10 @@ const CheckboxBase = forwardRef( ); - } + }, ); -CheckboxBase.displayName = 'Checkbox'; +CheckboxBase.displayName = "Checkbox"; // Checkbox.Item for use within Checkbox.Group const CheckboxItem = forwardRef( @@ -349,35 +349,35 @@ const CheckboxItem = forwardRef( checked, indeterminate, disabled, - variant = 'default', + variant = "default", label, value, onCheckedChange, - name + name, }, - ref + ref, ) => { const { controlFirst, appearance } = useContext(CheckboxGroupContext); - const isBordered = appearance === 'bordered'; + const isBordered = appearance === "bordered"; return ( ); - } + }, ); -CheckboxItem.displayName = 'Checkbox.Item'; +CheckboxItem.displayName = "Checkbox.Item"; // Checkbox.Legend — composable legend sub-component for Checkbox.Group function CheckboxLegend({ children, className }: CheckboxLegendProps) { return ( {children} ); } -CheckboxLegend.displayName = 'Checkbox.Legend'; +CheckboxLegend.displayName = "Checkbox.Legend"; // Checkbox.Group with built-in Fieldset and CheckboxGroup function CheckboxGroup({ @@ -452,12 +452,12 @@ function CheckboxGroup({ onValueChange, allValues, disabled, - orientation = 'vertical', - appearance = 'default', + orientation = "vertical", + appearance = "default", controlFirst, - className + className, }: CheckboxGroupProps) { - const effectiveControlFirst = controlFirst ?? appearance === 'default'; + const effectiveControlFirst = controlFirst ?? appearance === "default"; return ( - + {legend && ( {legend} @@ -480,16 +480,16 @@ function CheckboxGroup({ data-kumo-component="Checkbox" data-kumo-part="group-items" className={cn( - appearance === 'bordered' + appearance === "bordered" ? cn( - 'overflow-hidden rounded-lg border border-kumo-hairline bg-kumo-base', - orientation === 'vertical' - ? 'flex flex-col [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-t [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-kumo-line' - : 'flex flex-row [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-l [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-kumo-line' + "overflow-hidden rounded-lg border border-kumo-hairline bg-kumo-base", + orientation === "vertical" + ? "flex flex-col [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-t [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-kumo-line" + : "flex flex-row [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-l [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-kumo-line", ) - : orientation === 'vertical' - ? 'flex flex-col gap-2' - : 'flex flex-row flex-wrap gap-2' + : orientation === "vertical" + ? "flex flex-col gap-2" + : "flex flex-row flex-wrap gap-2", )} > {children} @@ -508,7 +508,7 @@ function CheckboxGroup({ export const Checkbox = Object.assign(CheckboxBase, { Item: CheckboxItem, Group: CheckboxGroup, - Legend: CheckboxLegend + Legend: CheckboxLegend, }); -Checkbox.displayName = 'Checkbox'; +Checkbox.displayName = "Checkbox"; diff --git a/packages/kumo/src/components/radio/radio.tsx b/packages/kumo/src/components/radio/radio.tsx index 071b62dffe..4fc2d2b0d0 100644 --- a/packages/kumo/src/components/radio/radio.tsx +++ b/packages/kumo/src/components/radio/radio.tsx @@ -384,6 +384,8 @@ function _RadioItem( data-kumo-part="item-label" className={cn( "group relative m-0 inline-flex items-start gap-2", + isBordered && + "w-full flex-1 p-3 transition-colors has-[[data-checked]]:bg-kumo-elevated", // "start" (default): radio before label // "end": label before radio using flex-row-reverse effectiveControlPosition === "end" && "flex-row-reverse justify-end", @@ -416,7 +418,15 @@ function _RadioItem( - {label} + + {label} + ); } From fd630740c5ad6ca9c4c7a3b1af67ed5f4d287ced Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Fri, 24 Jul 2026 13:30:49 -0400 Subject: [PATCH 3/5] chore(checkbox,radio): remove unrelated sidebar demo changes from WIP branch --- .../src/components/demos/SidebarDemo.tsx | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/packages/kumo-docs-astro/src/components/demos/SidebarDemo.tsx b/packages/kumo-docs-astro/src/components/demos/SidebarDemo.tsx index 39635652ba..18ff96bb0a 100644 --- a/packages/kumo-docs-astro/src/components/demos/SidebarDemo.tsx +++ b/packages/kumo-docs-astro/src/components/demos/SidebarDemo.tsx @@ -304,61 +304,6 @@ export function SidebarLoadingDemo() { ); } -function SidebarLoadingSkeleton() { - return ( - <> - -
-
-
-
- - - {["72%", "58%"].map((labelWidth, groupIndex) => ( - -
-
- {["68%", "82%", "55%"].map((width, rowIndex) => ( -
-
-
-
- ))} -
- - ))} - - - - - - ); -} - -/** Concept loading state that mirrors sidebar groups in expanded and collapsed modes. */ -export function SidebarLoadingDemo() { - return ( - - - - - - - -

Toggle to preview both loading states

-
-
-
- ); -} - // --------------------------------------------------------------------------- // 3. Resizable — drag handle with auto-collapse // --------------------------------------------------------------------------- From 6262bda9d09f574ccbf0df5b2a0a65e89ee59649 Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Fri, 24 Jul 2026 14:07:02 -0400 Subject: [PATCH 4/5] fix(radio): restore bordered appearance after rebase onto main --- packages/kumo/src/components/radio/radio.tsx | 48 +++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/kumo/src/components/radio/radio.tsx b/packages/kumo/src/components/radio/radio.tsx index 4fc2d2b0d0..5d79c47b81 100644 --- a/packages/kumo/src/components/radio/radio.tsx +++ b/packages/kumo/src/components/radio/radio.tsx @@ -45,6 +45,11 @@ export const KUMO_RADIO_VARIANTS = { description: "Choice card appearance with border, padding, and highlighted selection state", }, + bordered: { + classes: + "p-3 transition-colors hover:bg-kumo-elevated has-[[data-checked]]:bg-kumo-elevated", + description: "Option within a bordered group with shared dividers", + }, }, } as const; @@ -69,6 +74,7 @@ export interface KumoRadioVariantsProps { * Visual appearance. * - `"default"` — Standard inline radio item * - `"card"` — Choice card with border, padding, and highlighted selection state + * - `"bordered"` — Option within a bordered group with shared dividers * @default "default" */ appearance?: KumoRadioAppearance; @@ -100,7 +106,7 @@ export type RadioControlPosition = "start" | "end"; // Context for passing controlPosition and appearance from Group to Items. // `controlPosition` may be undefined so each item can fall back to an -// appearance-appropriate default (start for default, end for card). +// appearance-appropriate default (start for default, end for card/bordered). const RadioGroupContext = createContext<{ controlPosition: RadioControlPosition | undefined; appearance: KumoRadioAppearance; @@ -220,6 +226,7 @@ export interface RadioGroupProps { * Visual appearance applied to all Radio.Item children. * - `"default"` — Standard inline radio items * - `"card"` — Choice card with border, padding, and highlighted selection state + * - `"bordered"` — Contiguous bordered group with dividers between items * * Individual items can override this with their own `appearance` prop. * @default "default" @@ -243,7 +250,7 @@ export interface RadioGroupProps { ) => void; /** Whether all radios in the group are disabled */ disabled?: boolean; - /** Position of radio control relative to label: "start" puts radio before label, "end" puts label before radio. Defaults to "start" for default appearance and "end" for card appearance. */ + /** Position of radio control relative to label: "start" puts radio before label, "end" puts label before radio. Defaults to "start" for default appearance and "end" for card or bordered appearance. */ controlPosition?: RadioControlPosition; /** Form submission name for the radio group */ name?: string; @@ -279,6 +286,7 @@ export type RadioItemProps = { * Visual appearance of the radio item. * - `"default"` — Standard inline radio item * - `"card"` — Choice card with border, padding, and highlighted selection state + * - `"bordered"` — Option within a contiguous bordered group * * When set on an individual item, overrides the group-level `appearance`. * @default "default" @@ -313,12 +321,13 @@ function _RadioItem( useContext(RadioGroupContext); const appearance = appearanceProp ?? groupAppearance; const isCard = appearance === "card"; + const isBordered = appearance === "bordered"; // Fall back to an appearance-appropriate default when controlPosition is - // not provided: card defaults to "end" (radio on the right), default - // appearance defaults to "start" (radio on the left). + // not provided: card and bordered default to "end" (radio on the right), + // while default appearance puts the radio on the left. const effectiveControlPosition: RadioControlPosition = - controlPosition ?? (isCard ? "end" : "start"); + controlPosition ?? (isCard || isBordered ? "end" : "start"); if (isCard) { const controlAtStart = effectiveControlPosition === "start"; @@ -389,7 +398,12 @@ function _RadioItem( // "start" (default): radio before label // "end": label before radio using flex-row-reverse effectiveControlPosition === "end" && "flex-row-reverse justify-end", - disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer", + disabled + ? cn( + "cursor-not-allowed", + isBordered ? "[&>*]:opacity-50" : "opacity-50", + ) + : cn("cursor-pointer", isBordered && "hover:bg-kumo-elevated"), className, )} > @@ -490,12 +504,24 @@ function RadioGroup({ )}
[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-t [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-kumo-line" + : "flex flex-row [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-l [&>[data-kumo-part=item-label]+[data-kumo-part=item-label]]:border-kumo-line", + ) + : orientation === "vertical" + ? cn( + "flex flex-col", + appearance === "card" ? "gap-3" : "gap-2", + ) + : appearance === "card" + ? "grid grid-cols-2 gap-3" + : "flex flex-row flex-wrap gap-2", )} > {children} From e8abcd7885bcc29628c65a34a4e992a097af193d Mon Sep 17 00:00:00 2001 From: Matt Rothenberg Date: Fri, 24 Jul 2026 15:25:23 -0400 Subject: [PATCH 5/5] style(checkbox,radio): remove selected bg fill from bordered items --- packages/kumo/src/components/checkbox/checkbox.tsx | 3 +-- packages/kumo/src/components/radio/radio.tsx | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/kumo/src/components/checkbox/checkbox.tsx b/packages/kumo/src/components/checkbox/checkbox.tsx index 3b29a97894..c801799bed 100644 --- a/packages/kumo/src/components/checkbox/checkbox.tsx +++ b/packages/kumo/src/components/checkbox/checkbox.tsx @@ -366,8 +366,7 @@ const CheckboxItem = forwardRef( data-kumo-part="item-label" className={cn( "relative m-0 inline-flex items-start gap-2", - isBordered && - "w-full flex-1 p-3 transition-colors has-[[data-checked]]:bg-kumo-elevated", + isBordered && "w-full flex-1 p-3 transition-colors", // Control first (default): checkbox before label // Label first: label before checkbox using flex-row-reverse !controlFirst && "flex-row-reverse justify-end", diff --git a/packages/kumo/src/components/radio/radio.tsx b/packages/kumo/src/components/radio/radio.tsx index 5d79c47b81..bd21d5edad 100644 --- a/packages/kumo/src/components/radio/radio.tsx +++ b/packages/kumo/src/components/radio/radio.tsx @@ -46,8 +46,7 @@ export const KUMO_RADIO_VARIANTS = { "Choice card appearance with border, padding, and highlighted selection state", }, bordered: { - classes: - "p-3 transition-colors hover:bg-kumo-elevated has-[[data-checked]]:bg-kumo-elevated", + classes: "p-3 transition-colors hover:bg-kumo-elevated", description: "Option within a bordered group with shared dividers", }, }, @@ -393,8 +392,7 @@ function _RadioItem( data-kumo-part="item-label" className={cn( "group relative m-0 inline-flex items-start gap-2", - isBordered && - "w-full flex-1 p-3 transition-colors has-[[data-checked]]:bg-kumo-elevated", + isBordered && "w-full flex-1 p-3 transition-colors", // "start" (default): radio before label // "end": label before radio using flex-row-reverse effectiveControlPosition === "end" && "flex-row-reverse justify-end",