From c00f6eb784e0da41df14e0a7187bd22b7bfa0f00 Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Mon, 6 Jul 2026 14:05:21 +0530 Subject: [PATCH 01/31] fix(badge): update badge variant to use rounded-sm for improved styling --- packages/propel/src/elements/badge/variants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/propel/src/elements/badge/variants.ts b/packages/propel/src/elements/badge/variants.ts index 8acb90b9..bfab9f1e 100644 --- a/packages/propel/src/elements/badge/variants.ts +++ b/packages/propel/src/elements/badge/variants.ts @@ -16,7 +16,7 @@ import { type StrictVariantProps } from "../../internal/variant-props"; // - magnitude: S / Base / Large (height, horizontal padding, text size, node size) // - tone: color/sentiment (neutral, grey, brand, info, …) export const badgeVariants = cva( - "inline-flex w-fit shrink-0 items-center justify-center gap-1 rounded-full leading-none font-medium whitespace-nowrap", + "inline-flex w-fit shrink-0 items-center justify-center gap-1 rounded-sm leading-none font-medium whitespace-nowrap", { variants: { // Figma Size axis. Each step sets height, horizontal padding, text size, and the From 232b256cce4e50e993af5d210410730e5be9a3e3 Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Mon, 6 Jul 2026 14:27:11 +0530 Subject: [PATCH 02/31] refactor: wrap checkbox, radio, and switch controls in FieldItemControlGroup for consistent layout - Added FieldItemControlGroup to CheckboxField, CheckboxGroupFieldOption, RadioGroupFieldOption, and SwitchField components to enhance layout consistency. - Updated field item content variants to align control heights with label text for improved visual alignment. --- .../checkbox-field/checkbox-field.tsx | 5 ++++- .../checkbox-group-field-option.tsx | 5 ++++- .../radio-group-field-option.tsx | 5 ++++- .../components/switch-field/switch-field.tsx | 5 ++++- .../field/field-item-control-group.tsx | 21 +++++++++++++++++++ packages/propel/src/elements/field/index.tsx | 1 + .../propel/src/elements/field/variants.ts | 11 ++++++++-- 7 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 packages/propel/src/elements/field/field-item-control-group.tsx diff --git a/packages/propel/src/components/checkbox-field/checkbox-field.tsx b/packages/propel/src/components/checkbox-field/checkbox-field.tsx index 2e4428b1..3e5c11e4 100644 --- a/packages/propel/src/components/checkbox-field/checkbox-field.tsx +++ b/packages/propel/src/components/checkbox-field/checkbox-field.tsx @@ -1,5 +1,6 @@ import type * as React from "react"; +import { FieldItemControlGroup } from "../../elements/field/field-item-control-group"; import type { FieldMagnitude } from "../../elements/field/variants"; import { CheckboxFieldControl, @@ -40,7 +41,9 @@ export function CheckboxField({ return ( - + + + {label} diff --git a/packages/propel/src/components/checkbox-group-field/checkbox-group-field-option.tsx b/packages/propel/src/components/checkbox-group-field/checkbox-group-field-option.tsx index 73d7c847..4096edec 100644 --- a/packages/propel/src/components/checkbox-group-field/checkbox-group-field-option.tsx +++ b/packages/propel/src/components/checkbox-group-field/checkbox-group-field-option.tsx @@ -1,5 +1,6 @@ import type * as React from "react"; +import { FieldItemControlGroup } from "../../elements/field/field-item-control-group"; import type { FieldMagnitude } from "../../elements/field/variants"; import { CheckboxFieldControl, @@ -31,7 +32,9 @@ export function CheckboxGroupFieldOption({ return ( - + + + {label} diff --git a/packages/propel/src/components/radio-group-field/radio-group-field-option.tsx b/packages/propel/src/components/radio-group-field/radio-group-field-option.tsx index 88e8dd82..37c8d595 100644 --- a/packages/propel/src/components/radio-group-field/radio-group-field-option.tsx +++ b/packages/propel/src/components/radio-group-field/radio-group-field-option.tsx @@ -1,5 +1,6 @@ import type * as React from "react"; +import { FieldItemControlGroup } from "../../elements/field/field-item-control-group"; import type { FieldMagnitude } from "../../elements/field/variants"; import { useFieldOptionMagnitude } from "../../internal/field-option-magnitude"; import { FieldItem, FieldItemContent } from "../field"; @@ -25,7 +26,9 @@ export function RadioGroupFieldOption({ return ( - + + + {label} diff --git a/packages/propel/src/components/switch-field/switch-field.tsx b/packages/propel/src/components/switch-field/switch-field.tsx index 9ebaa026..cc245362 100644 --- a/packages/propel/src/components/switch-field/switch-field.tsx +++ b/packages/propel/src/components/switch-field/switch-field.tsx @@ -1,5 +1,6 @@ import type * as React from "react"; +import { FieldItemControlGroup } from "../../elements/field/field-item-control-group"; import type { FieldMagnitude } from "../../elements/field/variants"; import { Field, FieldItem, FieldItemContent } from "../field"; import { FieldHelperText } from "../field/field-helper-text"; @@ -38,7 +39,9 @@ export function SwitchField({ return ( - + + + {label} diff --git a/packages/propel/src/elements/field/field-item-control-group.tsx b/packages/propel/src/elements/field/field-item-control-group.tsx new file mode 100644 index 00000000..62dcd5e9 --- /dev/null +++ b/packages/propel/src/elements/field/field-item-control-group.tsx @@ -0,0 +1,21 @@ +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; + +import { fieldItemControlGroupVariants } from "./variants"; + +export type FieldItemControlGroupProps = Omit< + useRender.ComponentProps<"div">, + "className" | "style" +>; + +/** + * The control slot of a choice option row (checkbox/radio/switch): a 20px-tall box matching the + * label's first line box, so the wrapped control centers on that line even when the label wraps or + * a description follows. + */ +export function FieldItemControlGroup({ render, ...props }: FieldItemControlGroupProps) { + const defaultProps: useRender.ElementProps<"div"> = { + className: fieldItemControlGroupVariants(), + }; + return useRender({ defaultTagName: "div", render, props: mergeProps(defaultProps, props) }); +} diff --git a/packages/propel/src/elements/field/index.tsx b/packages/propel/src/elements/field/index.tsx index 1e75e31d..6444e674 100644 --- a/packages/propel/src/elements/field/index.tsx +++ b/packages/propel/src/elements/field/index.tsx @@ -3,6 +3,7 @@ export * from "./field-control-content"; export * from "./field-description"; export * from "./field-error"; export * from "./field-item"; +export * from "./field-item-control-group"; export * from "./field-item-content"; export * from "./field-label"; export * from "./field-label-group"; diff --git a/packages/propel/src/elements/field/variants.ts b/packages/propel/src/elements/field/variants.ts index c8c01cf0..7811b9f8 100644 --- a/packages/propel/src/elements/field/variants.ts +++ b/packages/propel/src/elements/field/variants.ts @@ -38,8 +38,15 @@ export type FieldLabelVariantProps = StrictVariantProps Date: Mon, 6 Jul 2026 14:39:45 +0530 Subject: [PATCH 03/31] refactor(autocomplete, combobox, select): update description prop behavior to handle error state - Modified the description prop in AutocompleteField, ComboboxField, and SelectField components to clarify that it is replaced by the error prop when an error is set. - Adjusted rendering logic to ensure description is only displayed when there is no error present. --- .../src/components/autocomplete-field/autocomplete-field.tsx | 4 ++-- .../propel/src/components/combobox-field/combobox-field.tsx | 4 ++-- packages/propel/src/components/select-field/select-field.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/propel/src/components/autocomplete-field/autocomplete-field.tsx b/packages/propel/src/components/autocomplete-field/autocomplete-field.tsx index 4824bd41..722030c5 100644 --- a/packages/propel/src/components/autocomplete-field/autocomplete-field.tsx +++ b/packages/propel/src/components/autocomplete-field/autocomplete-field.tsx @@ -20,7 +20,7 @@ export type AutocompleteFieldProps = Omit< AutocompleteProps, "children" | "items" > & { - /** Supporting text shown below the input. */ + /** Supporting text shown below the input. Replaced by `error` when an error is set. */ description?: React.ReactNode; /** The clear control (e.g. an `IconButton`) carrying its own localizable `aria-label`. */ clear: React.ReactElement; @@ -76,7 +76,7 @@ export function AutocompleteField({ - {description != null ? ( + {error == null && description != null ? ( {description} ) : null} diff --git a/packages/propel/src/components/combobox-field/combobox-field.tsx b/packages/propel/src/components/combobox-field/combobox-field.tsx index 5d87c46f..9bf73bd7 100644 --- a/packages/propel/src/components/combobox-field/combobox-field.tsx +++ b/packages/propel/src/components/combobox-field/combobox-field.tsx @@ -16,7 +16,7 @@ export type ComboboxFieldProps = Omit< ComboboxProps, "children" | "items" > & { - /** Supporting text shown below the input. */ + /** Supporting text shown below the input. Replaced by `error` when an error is set. */ description?: React.ReactNode; /** * The clear control (e.g. an `IconButton`), rendered as the combobox's clear button. It carries @@ -74,7 +74,7 @@ export function ComboboxField({ - {description != null ? ( + {error == null && description != null ? ( {description} ) : null} diff --git a/packages/propel/src/components/select-field/select-field.tsx b/packages/propel/src/components/select-field/select-field.tsx index 3276c471..902c54b0 100644 --- a/packages/propel/src/components/select-field/select-field.tsx +++ b/packages/propel/src/components/select-field/select-field.tsx @@ -31,7 +31,7 @@ export type SelectFieldOption = { }; export type SelectFieldProps = Omit, "children" | "items"> & { - /** Supporting text shown below the trigger. */ + /** Supporting text shown below the trigger. Replaced by `error` when an error is set. */ description?: React.ReactNode; /** Error text shown below the control. */ error?: React.ReactNode; @@ -63,7 +63,7 @@ export function SelectField({ - }>{label} + {/* Use the shared `FieldLabel` (magnitude-scaled: md=text-13) so the select field's label + matches the other field labels — the picker's own fixed-`text-14` `SelectLabel` made it a + step too large next to Input/Radio/Checkbox in a form. `Field.Root` still associates it + with the trigger. */} + + {label} + {error == null && description != null ? ( {description} From ed29335412a5b5df0957ec128f3cca118188a8e4 Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 7 Jul 2026 11:13:56 +0530 Subject: [PATCH 16/31] refactor(checkbox, radio, switch): unify transition durations and enhance comments - Updated transition durations for Checkbox, Radio, and Switch components to a consistent 100ms for improved responsiveness. - Enhanced comments to clarify the rationale behind the changes, ensuring a cohesive user experience across toggle controls. - Added CheckboxField to the Checkbox stories for better integration and documentation of form behavior. --- .../components/checkbox/checkbox.stories.tsx | 40 +++++++++++++------ .../propel/src/elements/radio/variants.ts | 4 +- .../propel/src/elements/switch/variants.ts | 7 +++- packages/propel/src/internal/checkbox-box.ts | 16 ++++++-- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/packages/propel/src/components/checkbox/checkbox.stories.tsx b/packages/propel/src/components/checkbox/checkbox.stories.tsx index ca5d01a6..3fb3b202 100644 --- a/packages/propel/src/components/checkbox/checkbox.stories.tsx +++ b/packages/propel/src/components/checkbox/checkbox.stories.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import { expect, userEvent } from "storybook/test"; import { Button } from "../button"; +import { CheckboxField } from "../checkbox-field"; import { Field } from "../field"; import { Form, FormActions, FormBody } from "../form"; import { Icon } from "../icon"; @@ -17,10 +18,14 @@ import { const meta = { title: "Components/Checkbox", component: Checkbox, + // The ready-made Checkbox is the bare box (+ optional inline label); CheckboxField is the + // labeled-row composition that owns the Field name for form submission — add its tab to the + // args table and record the relationship in the manifest (mirrors Switch/RadioGroup). subcomponents: { CheckboxLabel, CheckboxIndicator, CheckboxIndeterminateIndicator, + CheckboxField, }, args: { "aria-label": "Example", @@ -164,9 +169,10 @@ export const Invalid: Story = { }; /** - * Interaction test: the invalid `Field` propagates `data-invalid` and the danger border, while the - * checked box keeps the accent fill. Tagged out of the sidebar/docs/manifest while still running - * under the default `test` tag. + * Interaction test: the invalid `Field` propagates `data-invalid` and the danger border on the + * RESTING box only — once checked, the danger border clears back to transparent (the accent fill + * alone communicates the value; a required-and-now-satisfied checkbox shouldn't still ring red). + * Tagged out of the sidebar/docs/manifest while still running under the default `test` tag. */ export const InvalidInteraction: Story = { ...Invalid, @@ -182,17 +188,29 @@ export const InvalidInteraction: Story = { await expect(getComputedStyle(unchecked).borderColor).not.toBe( getComputedStyle(resting).borderColor, ); - // Checked invalid box: accent-blue fill, like every other state. + // Checked invalid box: accent-blue fill, like every other state... await expect(checked).toHaveAttribute("aria-checked", "true"); - await expect(checked).toHaveClass("data-checked:bg-accent-primary"); + await expect(checked).toHaveAttribute("data-invalid"); + await expect(getComputedStyle(checked).backgroundColor).not.toBe( + getComputedStyle(resting).backgroundColor, + ); + // ...and, despite still being `data-invalid`, the danger border does NOT persist through the + // checked fill: `data-checked:border-transparent` takes over, same as a checked box anywhere + // else — it must NOT still show the unchecked invalid box's red border. + await expect(getComputedStyle(checked).borderColor).not.toBe( + getComputedStyle(unchecked).borderColor, + ); }, }; /** - * Form integration: wrap the checkbox in a `Field` with a `name` and Base UI wires the rest — the - * field name flows onto the box, a hidden input serializes it with the form, and `Form`'s - * `onFormSubmit` receives the checked state as a boolean. The checkbox itself needs no extra - * wiring. Submit to see the captured value. + * Form integration: a sign-in form where "Stay logged in" needs to submit alongside the rest of the + * fields — e.g. to extend the session server-side once the user authenticates. `CheckboxField` (the + * labeled-row ready-made) already owns the `Field` name, so inside a `Form` its checked state + * serializes with the submission — `onFormSubmit` receives it under the field's `name` as a + * boolean, with no extra wiring on the checkbox. Reach for the bare `Checkbox` + a hand-wired + * `Field` (as in the `Invalid` story above) only when you need a custom row layout `CheckboxField` + * doesn't offer. Submit to see the captured value. */ export const FormIntegration: Story = { parameters: { controls: { disable: true } }, @@ -202,9 +220,7 @@ export const FormIntegration: Story = {
onFormSubmit={(values) => setSubmitted(values)}> - - - +