From 01edf61d5da5f9a0887a67564bc8c196b90a5d01 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Mon, 19 Oct 2026 08:41:44 +0530 Subject: [PATCH 1/2] refactor: replace one-off props with shared names Remove Command.DialogContent width and Search width. Rename Skeleton borderRadius to radius and Breadcrumb.Item current to active. Merge DatePicker and FilterChip onErrorChange into onValidityChange, as on CalendarPreview.Input. Add gap 0 to Flex and Grid, and radius to Toast. --- .agents/skills/apsara/references/tokens.md | 2 +- apps/www/src/components/docs/navbar.tsx | 2 +- apps/www/src/components/docs/search.tsx | 5 +- .../src/content/docs/(overview)/upgrading.mdx | 56 +++++++++ .../docs/components/breadcrumb/demo.ts | 18 +-- .../docs/components/breadcrumb/index.mdx | 8 +- .../docs/components/breadcrumb/props.ts | 2 +- .../content/docs/components/calendar/demo.ts | 2 +- .../content/docs/components/calendar/props.ts | 20 ++-- .../content/docs/components/command/index.mdx | 6 + .../content/docs/components/command/props.ts | 4 +- .../docs/components/filter-chip/props.ts | 8 +- .../src/content/docs/components/flex/demo.ts | 2 +- .../src/content/docs/components/flex/props.ts | 3 +- .../src/content/docs/components/grid/demo.ts | 2 +- .../src/content/docs/components/grid/props.ts | 5 +- .../content/docs/components/search/index.mdx | 10 ++ .../content/docs/components/skeleton/demo.ts | 2 +- .../docs/components/skeleton/index.mdx | 2 +- .../content/docs/components/skeleton/props.ts | 6 +- .../content/docs/components/toast/index.mdx | 2 +- .../content/docs/components/toast/props.ts | 6 + .../breadcrumb/__tests__/breadcrumb.test.tsx | 10 +- .../breadcrumb/__tests__/data-slots.test.tsx | 4 +- .../components/breadcrumb/breadcrumb-item.tsx | 10 +- .../calendar/__tests__/date-picker.test.tsx | 111 ++++++++++++------ .../react/components/calendar/date-picker.tsx | 43 ++++--- .../components/command/command-dialog.tsx | 4 - .../__tests__/filter-chip.test.tsx | 18 ++- .../components/filter-chip/filter-chip.tsx | 13 +- .../components/flex/__tests__/flex.test.tsx | 2 +- .../components/grid/__tests__/grid.test.tsx | 2 +- .../search/__tests__/search.test.tsx | 15 --- packages/react/components/search/search.tsx | 10 +- .../skeleton/__tests__/skeleton.test.tsx | 27 +---- .../components/skeleton/skeleton.module.css | 2 + .../react/components/skeleton/skeleton.tsx | 8 +- .../components/toast/__tests__/toast.test.tsx | 12 ++ .../react/components/toast/toast-provider.tsx | 21 +++- .../react/components/toast/toast-root.tsx | 6 +- packages/react/figma/breadcrumb-item.figma.ts | 8 +- packages/react/figma/breadcrumb.figma.ts | 2 +- packages/react/shared/gap/gap.module.css | 9 ++ packages/react/shared/gap/gap.ts | 3 + 44 files changed, 331 insertions(+), 182 deletions(-) diff --git a/.agents/skills/apsara/references/tokens.md b/.agents/skills/apsara/references/tokens.md index 4a22655ad..c39cef8fa 100644 --- a/.agents/skills/apsara/references/tokens.md +++ b/.agents/skills/apsara/references/tokens.md @@ -113,7 +113,7 @@ Use for padding, margin, and gap. Values are in `px`. | `--rs-space-8` | 28 | | `--rs-space-17` | 120 | | `--rs-space-9` | 32 | | | | -> Layout components (`Flex`, `Grid`, `Box`) accept numeric `gap`/`padding` props that map to this scale (e.g. `gap={4}` → `--rs-space-4` = 12px). +> Layout components (`Flex`, `Grid`, `Box`) accept numeric `gap`/`padding` props that map to this scale (e.g. `gap={4}` → `--rs-space-4` = 12px). `gap={0}` removes the gap. --- diff --git a/apps/www/src/components/docs/navbar.tsx b/apps/www/src/components/docs/navbar.tsx index fe10bd3e3..700c13bea 100644 --- a/apps/www/src/components/docs/navbar.tsx +++ b/apps/www/src/components/docs/navbar.tsx @@ -78,7 +78,7 @@ export default function DocsNavbar({ /> ))} - {title} + {title} )} diff --git a/apps/www/src/components/docs/search.tsx b/apps/www/src/components/docs/search.tsx index e66779c03..7f4c1ff50 100644 --- a/apps/www/src/components/docs/search.tsx +++ b/apps/www/src/components/docs/search.tsx @@ -198,7 +198,10 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { > - + … + + +Profile + +// After +… +
+ +
+ +Profile +``` + +`Skeleton` keeps its default corner radius. For a value outside the radius +scale, set `style={{ borderRadius }}`. + +The active `Breadcrumb.Item` has `data-active="true"` in place of +`data-current="true"`. Update CSS that targets the old attribute. + +### One callback for typed dates + +`DatePicker`, `FilterChip` and `CalendarPreview.Input` report the validity of +typed text with `onValidityChange`. It receives `{ valid, reason, message }`. +`message` is `undefined` while the text is valid, so read `message` where you +used the old error string: + +```tsx +// Before + + +// After + setError(message)} /> +``` + +`DatePicker` reports the reason `unparseable` or `out-of-bounds`, with the +message `'Invalid date'`. The callback also fires when the reason changes and +the text stays invalid. + ## 2.0: `Theme` is rewritten `Theme` used to put its tokens on `` from an effect, so it could not diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts index ad8ce10b7..a76b9792f 100644 --- a/apps/www/src/content/docs/components/breadcrumb/demo.ts +++ b/apps/www/src/content/docs/components/breadcrumb/demo.ts @@ -9,7 +9,7 @@ export const getCode = (props: ComponentPropsType) => { Products - Shoes + Shoes `; }; @@ -36,7 +36,7 @@ export const sizeDemo = { Products - Shoes + Shoes ` }, { @@ -47,7 +47,7 @@ export const sizeDemo = { Products - Shoes + Shoes ` } ] @@ -62,14 +62,14 @@ export const separatorDemo = { | Products | - Shoes + Shoes Home - Products - - Shoes + Shoes ` }; @@ -82,7 +82,7 @@ export const ellipsisDemo = { - Shoes + Shoes ` }; @@ -115,7 +115,7 @@ export const dropdownLinksDemo = { { children: 'Books', onClick: () => {console.log('Books')}} ]}>Categories - Current + Current ` }; @@ -127,7 +127,7 @@ export const asDemo = { }>Playground - Docs + Docs ` }; @@ -139,7 +139,7 @@ export const disabledDemo = { Loading… - Products + Products ` }; diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx index cc8b5e010..ced74a5f4 100644 --- a/apps/www/src/content/docs/components/breadcrumb/index.mdx +++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx @@ -100,19 +100,19 @@ Groups all parts of the breadcrumb navigation. ### Item -Renders an individual breadcrumb link. Use the `current` prop on the item that represents the current page so it is styled and exposed to assistive tech (e.g. `aria-current="page"`). Use the `disabled` prop for non-clickable, visually muted items (e.g. loading or no access). +Renders an individual breadcrumb link. Use the `active` prop on the item that represents the current page so it is styled and exposed to assistive tech (e.g. `aria-current="page"`). Use the `disabled` prop for non-clickable, visually muted items (e.g. loading or no access). -Item elements expose data attributes for CSS state targeting so you can style current and disabled states without relying on internal class names: +Item elements expose data attributes for CSS state targeting so you can style active and disabled states without relying on internal class names: | Attribute | When present | |-----------|----------------| -| `data-current="true"` | Item is the current page (`current` prop) | +| `data-active="true"` | Item is the current page (`active` prop) | | `data-disabled="true"` | Item is disabled (`disabled` prop) | Example: ```css -[data-current="true"] { +[data-active="true"] { color: var(--my-brand); } [data-disabled="true"] { diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts index eba73e64f..63e94c876 100644 --- a/apps/www/src/content/docs/components/breadcrumb/props.ts +++ b/apps/www/src/content/docs/components/breadcrumb/props.ts @@ -17,7 +17,7 @@ export interface BreadcrumbItem { * Whether the item is the current page * @defaultValue false */ - current?: boolean; + active?: boolean; /** * When true, the item is non-clickable and visually muted (e.g. loading or no access). diff --git a/apps/www/src/content/docs/components/calendar/demo.ts b/apps/www/src/content/docs/components/calendar/demo.ts index 42b611b85..f1b8fbbe6 100644 --- a/apps/www/src/content/docs/components/calendar/demo.ts +++ b/apps/www/src/content/docs/components/calendar/demo.ts @@ -197,7 +197,7 @@ function DatePickerFieldExample() { style={{ maxWidth: 240 }} > setError(message)} slotProps={{ input: { size: "medium" } }} /> diff --git a/apps/www/src/content/docs/components/calendar/props.ts b/apps/www/src/content/docs/components/calendar/props.ts index e3f047615..cc29b83c1 100644 --- a/apps/www/src/content/docs/components/calendar/props.ts +++ b/apps/www/src/content/docs/components/calendar/props.ts @@ -250,14 +250,20 @@ export interface DatePickerProps { onSelect?: (date: Date) => void; /** - * Fires when the typed-input validation state changes: with a message when - * the typed text stops parsing as a valid in-bounds date, and with - * `undefined` when it becomes valid again (or the picker commits/closes). - * DatePicker renders no error UI of its own — not even `aria-invalid`. - * Lift this into `Field`'s `error` prop (or your form library) to display - * it; `Field` also wires `aria-invalid` onto the input. + * Fires when the typed-input validity changes. An invalid validity has a + * `reason` (`unparseable`, or `out-of-bounds` outside `startMonth` and + * `endMonth`) and the message `'Invalid date'`. It is `{ valid: true }` when + * the text is valid again or the picker commits or closes. + * DatePicker renders no error UI of its own, not even `aria-invalid`. + * Lift `message` into `Field`'s `error` prop (or your form library) to + * display it; `Field` also wires `aria-invalid` onto the input. + * @example onValidityChange={({ message }) => setError(message)} */ - onErrorChange?: (error: string | undefined) => void; + onValidityChange?: (validity: { + valid: boolean; + reason?: 'unparseable' | 'out-of-bounds' | 'unavailable' | 'out-of-order'; + message?: string; + }) => void; /** @deprecated Use `slotProps.calendar` instead. */ calendarProps?: CalendarProps; diff --git a/apps/www/src/content/docs/components/command/index.mdx b/apps/www/src/content/docs/components/command/index.mdx index 89159adf4..3cff61ad8 100644 --- a/apps/www/src/content/docs/components/command/index.mdx +++ b/apps/www/src/content/docs/components/command/index.mdx @@ -148,6 +148,12 @@ Visual divider between groups. The separator is hidden automatically while the u Renders the portal, viewport, and popup for the command palette. There is no backdrop overlay. Unlike `Dialog.Content`, there is no close button; users dismiss the palette via `Escape` or by clicking outside it. +The popup is 540px wide. To change the width, pass `className` or `style`, as on `Dialog.Content`: + +```tsx + +``` + ### Slots diff --git a/apps/www/src/content/docs/components/command/props.ts b/apps/www/src/content/docs/components/command/props.ts index d98976187..56f184f65 100644 --- a/apps/www/src/content/docs/components/command/props.ts +++ b/apps/www/src/content/docs/components/command/props.ts @@ -155,8 +155,8 @@ export interface CommandDialogContentProps { /** Additional CSS class names applied to the dialog popup. */ className?: string; - /** Explicit width for the dialog popup. */ - width?: string | number; + /** Inline styles applied to the dialog popup. */ + style?: React.CSSProperties; /** * Corner radius for this palette only. Overrides the theme's `radius`. diff --git a/apps/www/src/content/docs/components/filter-chip/props.ts b/apps/www/src/content/docs/components/filter-chip/props.ts index 161f1f715..9354b53f3 100644 --- a/apps/www/src/content/docs/components/filter-chip/props.ts +++ b/apps/www/src/content/docs/components/filter-chip/props.ts @@ -77,8 +77,12 @@ export interface FilterChipProps { * @default false */ showCalendarIcon?: boolean; - /** Called with a message when the typed date is invalid, and with `undefined` when it is valid again. */ - onErrorChange?: (error: string | undefined) => void; + /** Called when the typed date becomes invalid or valid again. Same as `onValidityChange` on `CalendarPreview.Input`. */ + onValidityChange?: (validity: { + valid: boolean; + reason?: 'unparseable' | 'out-of-bounds' | 'unavailable' | 'out-of-order'; + message?: string; + }) => void; }; /** Additional CSS class names */ diff --git a/apps/www/src/content/docs/components/flex/demo.ts b/apps/www/src/content/docs/components/flex/demo.ts index d01d5cc5d..84653cda2 100644 --- a/apps/www/src/content/docs/components/flex/demo.ts +++ b/apps/www/src/content/docs/components/flex/demo.ts @@ -16,7 +16,7 @@ export const playground = { controls: { gap: { type: 'select', - options: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], initialValue: 3 }, wrap: { diff --git a/apps/www/src/content/docs/components/flex/props.ts b/apps/www/src/content/docs/components/flex/props.ts index 27a2e4853..83ba0a6b2 100644 --- a/apps/www/src/content/docs/components/flex/props.ts +++ b/apps/www/src/content/docs/components/flex/props.ts @@ -1,9 +1,10 @@ export interface FlexProps { /** * Sets the gutter space between rows and columns. - * Each step maps directly to `--rs-space-1` through `--rs-space-17`. + * `0` removes the gap. Steps 1 to 17 map to `--rs-space-1` through `--rs-space-17`. */ gap?: + | 0 | 1 | 2 | 3 diff --git a/apps/www/src/content/docs/components/grid/demo.ts b/apps/www/src/content/docs/components/grid/demo.ts index 6f7eb6644..4c1ad2cfa 100644 --- a/apps/www/src/content/docs/components/grid/demo.ts +++ b/apps/www/src/content/docs/components/grid/demo.ts @@ -21,7 +21,7 @@ export const playground = { controls: { gap: { type: 'select', - options: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], initialValue: 3 }, rows: { diff --git a/apps/www/src/content/docs/components/grid/props.ts b/apps/www/src/content/docs/components/grid/props.ts index f909e3c5b..69d2f253e 100644 --- a/apps/www/src/content/docs/components/grid/props.ts +++ b/apps/www/src/content/docs/components/grid/props.ts @@ -40,9 +40,10 @@ export interface GridProps { /** * Sets the gap between grid items. - * Each step maps directly to `--rs-space-1` through `--rs-space-17`. + * `0` removes the gap. Steps 1 to 17 map to `--rs-space-1` through `--rs-space-17`. */ gap?: + | 0 | 1 | 2 | 3 @@ -65,6 +66,7 @@ export interface GridProps { * Sets the gap between grid columns. Uses the same scale as `gap`. */ columnGap?: + | 0 | 1 | 2 | 3 @@ -87,6 +89,7 @@ export interface GridProps { * Sets the gap between grid rows. Uses the same scale as `gap`. */ rowGap?: + | 0 | 1 | 2 | 3 diff --git a/apps/www/src/content/docs/components/search/index.mdx b/apps/www/src/content/docs/components/search/index.mdx index febe8ef1e..7c68f5009 100644 --- a/apps/www/src/content/docs/components/search/index.mdx +++ b/apps/www/src/content/docs/components/search/index.mdx @@ -28,6 +28,16 @@ Two sizes. `large` is the default; `small` fits a toolbar or a table header. +### Width + +Search fills the width of its parent. To set a width, put it in a sized element: + +```tsx +
+ +
+``` + ### Leading icon Pass a node to `leadingIcon` to replace the search icon, or `null` to hide it. diff --git a/apps/www/src/content/docs/components/skeleton/demo.ts b/apps/www/src/content/docs/components/skeleton/demo.ts index 573c3c594..685285920 100644 --- a/apps/www/src/content/docs/components/skeleton/demo.ts +++ b/apps/www/src/content/docs/components/skeleton/demo.ts @@ -95,7 +95,7 @@ export const providerDemo = { duration={2} > - + diff --git a/apps/www/src/content/docs/components/skeleton/index.mdx b/apps/www/src/content/docs/components/skeleton/index.mdx index ee712d151..9920f85ae 100644 --- a/apps/www/src/content/docs/components/skeleton/index.mdx +++ b/apps/www/src/content/docs/components/skeleton/index.mdx @@ -80,7 +80,7 @@ Create a card-like loading state by combining multiple skeletons. {/* Inherits provider props, overrides width */} {/* Overrides specific props */} ``` diff --git a/apps/www/src/content/docs/components/skeleton/props.ts b/apps/www/src/content/docs/components/skeleton/props.ts index 412955b17..23c7bbf7b 100644 --- a/apps/www/src/content/docs/components/skeleton/props.ts +++ b/apps/www/src/content/docs/components/skeleton/props.ts @@ -24,10 +24,10 @@ export interface SkeletonProps { highlightColor?: string; /** - * Border radius of the skeleton - * @defaultValue "var(--rs-radius-2)" + * Corner radius for this skeleton only. Overrides the theme's `radius`. + * @defaultValue The theme's `radius` */ - borderRadius?: string | number; + radius?: 'none' | 'small' | 'medium' | 'large' | 'full'; /** * Whether to display as inline-block diff --git a/apps/www/src/content/docs/components/toast/index.mdx b/apps/www/src/content/docs/components/toast/index.mdx index e5386a71c..7907adde1 100644 --- a/apps/www/src/content/docs/components/toast/index.mdx +++ b/apps/www/src/content/docs/components/toast/index.mdx @@ -94,7 +94,7 @@ The provider that renders toasts, and the manager APIs that add them, from a mod ### Toast.Provider -Renders the toast viewport and manages the toast lifecycle. Place once at the root of your app. +Renders the toast viewport and manages the toast lifecycle. Place once at the root of your app. `position` and `radius` apply to every toast it renders. diff --git a/apps/www/src/content/docs/components/toast/props.ts b/apps/www/src/content/docs/components/toast/props.ts index ebf6970a0..d63a97a5d 100644 --- a/apps/www/src/content/docs/components/toast/props.ts +++ b/apps/www/src/content/docs/components/toast/props.ts @@ -11,6 +11,12 @@ export interface ToastProviderProps { | 'top-center' | 'bottom-center'; + /** + * Corner radius for these toasts only. Overrides the theme's `radius`. + * @defaultValue The theme's `radius` + */ + radius?: 'none' | 'small' | 'medium' | 'large' | 'full'; + /** * Maximum number of visible toasts. * @default 3 diff --git a/packages/react/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/react/components/breadcrumb/__tests__/breadcrumb.test.tsx index 07c316b0a..372b255f8 100644 --- a/packages/react/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/react/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -178,10 +178,10 @@ describe('Breadcrumb', () => { expect(link?.textContent).toMatch(/L\s*Label\s*T/); }); - it('applies current/active state and renders as span with aria-current', () => { + it('applies active state and renders as span with aria-current', () => { const { container } = render( - Current Page + Current Page ); @@ -193,7 +193,7 @@ describe('Breadcrumb', () => { expect(currentEl).toHaveAttribute('aria-current', 'page'); expect(currentEl).toHaveClass(styles['breadcrumb-link']); expect(currentEl).toHaveClass(styles['breadcrumb-link-active']); - expect(currentEl).toHaveAttribute('data-current', 'true'); + expect(currentEl).toHaveAttribute('data-active', 'true'); expect(currentEl).toHaveTextContent('Current Page'); expect(container.querySelector('a')).not.toBeInTheDocument(); }); @@ -549,7 +549,7 @@ describe('Breadcrumb', () => { Electronics / - Laptop + Laptop ); @@ -596,7 +596,7 @@ describe('Breadcrumb', () => { Categories - Product + Product ); diff --git a/packages/react/components/breadcrumb/__tests__/data-slots.test.tsx b/packages/react/components/breadcrumb/__tests__/data-slots.test.tsx index 7ad4ca26c..166be94ed 100644 --- a/packages/react/components/breadcrumb/__tests__/data-slots.test.tsx +++ b/packages/react/components/breadcrumb/__tests__/data-slots.test.tsx @@ -16,7 +16,7 @@ describe('Breadcrumb data-slot contract', () => { - Current + Current ); expectSlots(container, [ @@ -42,7 +42,7 @@ describe('Breadcrumb data-slot contract', () => { expect(getSlot(container, 'breadcrumb-trailing-icon')).toBeNull(); }); - it('exposes a link slot on disabled/current items rendered as spans', () => { + it('exposes a link slot on disabled/active items rendered as spans', () => { const { container } = render( Disabled diff --git a/packages/react/components/breadcrumb/breadcrumb-item.tsx b/packages/react/components/breadcrumb/breadcrumb-item.tsx index 7a841967e..efc6363fd 100644 --- a/packages/react/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/react/components/breadcrumb/breadcrumb-item.tsx @@ -19,7 +19,7 @@ export type BreadcrumbDropdownItem = ComponentProps & { export interface BreadcrumbItemProps extends useRender.ComponentProps<'a'> { leadingIcon?: ReactNode; trailingIcon?: ReactNode; - current?: boolean; + active?: boolean; /** When true, the item is non-clickable and visually muted (e.g. loading or no access). */ disabled?: boolean; dropdownItems?: BreadcrumbDropdownItem[]; @@ -32,7 +32,7 @@ export const BreadcrumbItem = ({ className, leadingIcon, trailingIcon, - current, + active, disabled, href, dropdownItems, @@ -136,7 +136,7 @@ export const BreadcrumbItem = ({ ); } - if (disabled || current) { + if (disabled || active) { return (
  • {label} diff --git a/packages/react/components/calendar/__tests__/date-picker.test.tsx b/packages/react/components/calendar/__tests__/date-picker.test.tsx index 1a3f37329..4e3c74912 100644 --- a/packages/react/components/calendar/__tests__/date-picker.test.tsx +++ b/packages/react/components/calendar/__tests__/date-picker.test.tsx @@ -300,9 +300,12 @@ describe('DatePicker', () => { * are now allowed; bounds come from `startMonth` / `endMonth`. */ it('accepts a future date when no calendarProps bounds are set', () => { - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( - + ); const input = screen.getByPlaceholderText( @@ -310,16 +313,16 @@ describe('DatePicker', () => { ) as HTMLInputElement; // Pick a date far in the future to avoid clock skew in tests. fireEvent.change(input, { target: { value: '15/06/2099' } }); - expect(onErrorChange).not.toHaveBeenCalled(); + expect(onValidityChange).not.toHaveBeenCalled(); }); it('accepts a future date within endMonth', () => { - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( ); @@ -327,16 +330,16 @@ describe('DatePicker', () => { 'Select date' ) as HTMLInputElement; fireEvent.change(input, { target: { value: '15/06/2099' } }); - expect(onErrorChange).not.toHaveBeenCalled(); + expect(onValidityChange).not.toHaveBeenCalled(); }); it('rejects a date past endMonth', () => { - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( ); @@ -344,16 +347,20 @@ describe('DatePicker', () => { 'Select date' ) as HTMLInputElement; fireEvent.change(input, { target: { value: '15/06/2099' } }); - expect(onErrorChange).toHaveBeenLastCalledWith('Invalid date'); + expect(onValidityChange).toHaveBeenLastCalledWith({ + valid: false, + reason: 'out-of-bounds', + message: 'Invalid date' + }); }); it('rejects a date before startMonth', () => { - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( ); @@ -361,7 +368,11 @@ describe('DatePicker', () => { 'Select date' ) as HTMLInputElement; fireEvent.change(input, { target: { value: '15/06/2020' } }); - expect(onErrorChange).toHaveBeenLastCalledWith('Invalid date'); + expect(onValidityChange).toHaveBeenLastCalledWith({ + valid: false, + reason: 'out-of-bounds', + message: 'Invalid date' + }); }); }); @@ -414,9 +425,12 @@ describe('DatePicker', () => { }); it('accepts a fully-typed valid date matching dateFormat', () => { - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( - + ); const input = screen.getByPlaceholderText( @@ -425,7 +439,7 @@ describe('DatePicker', () => { fireEvent.change(input, { target: { value: '15/06/2025' } }); // The change handler accepted it, so no error transition fired. - expect(onErrorChange).not.toHaveBeenCalled(); + expect(onValidityChange).not.toHaveBeenCalled(); }); /* @@ -468,12 +482,12 @@ describe('DatePicker', () => { it('does not fire onSelect while typing, so partial stays uncommitted, valid waits for commit (Enter/blur/outside-click)', () => { const onSelect = vi.fn(); - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( ); @@ -489,34 +503,44 @@ describe('DatePicker', () => { // commit only happens via Enter / blur / outside-click (see the // dedicated single-fire test below for that path). fireEvent.change(input, { target: { value: '15/06/2025' } }); - expect(onErrorChange).toHaveBeenLastCalledWith(undefined); + expect(onValidityChange).toHaveBeenLastCalledWith({ valid: true }); expect(onSelect).not.toHaveBeenCalled(); }); }); - describe('onErrorChange', () => { + describe('onValidityChange', () => { /* * DatePicker renders no error message itself, and consumers lift validity * into `Field`'s `error` prop (or a form library) via this callback. * It fires on transitions only, like `onOpenChange`. */ it('fires with a message when typed text stops parsing as a date', () => { - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( - + ); const input = screen.getByPlaceholderText('Select date'); fireEvent.change(input, { target: { value: 'not a date' } }); - expect(onErrorChange).toHaveBeenCalledTimes(1); - expect(onErrorChange).toHaveBeenCalledWith('Invalid date'); + expect(onValidityChange).toHaveBeenCalledTimes(1); + expect(onValidityChange).toHaveBeenCalledWith({ + valid: false, + reason: 'unparseable', + message: 'Invalid date' + }); }); it('does not re-fire on consecutive invalid keystrokes', () => { - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( - + ); const input = screen.getByPlaceholderText('Select date'); @@ -524,39 +548,48 @@ describe('DatePicker', () => { fireEvent.change(input, { target: { value: '15/0' } }); fireEvent.change(input, { target: { value: '15/06' } }); - expect(onErrorChange).toHaveBeenCalledTimes(1); + expect(onValidityChange).toHaveBeenCalledTimes(1); }); - it('fires with undefined when the typed text becomes valid again', () => { - const onErrorChange = vi.fn(); + it('fires with valid when the typed text becomes valid again', () => { + const onValidityChange = vi.fn(); render( - + ); const input = screen.getByPlaceholderText('Select date'); fireEvent.change(input, { target: { value: '15/06' } }); fireEvent.change(input, { target: { value: '15/06/2025' } }); - expect(onErrorChange).toHaveBeenCalledTimes(2); - expect(onErrorChange).toHaveBeenLastCalledWith(undefined); + expect(onValidityChange).toHaveBeenCalledTimes(2); + expect(onValidityChange).toHaveBeenLastCalledWith({ valid: true }); }); it('never fires while typing stays valid', () => { - const onErrorChange = vi.fn(); + const onValidityChange = vi.fn(); render( - + ); const input = screen.getByPlaceholderText('Select date'); fireEvent.change(input, { target: { value: '15/06/2025' } }); - expect(onErrorChange).not.toHaveBeenCalled(); + expect(onValidityChange).not.toHaveBeenCalled(); }); - it('fires with undefined when the picker commits via Enter with invalid text', () => { - const onErrorChange = vi.fn(); + it('fires with valid when the picker commits via Enter with invalid text', () => { + const onValidityChange = vi.fn(); render( - + ); const input = screen.getByPlaceholderText('Select date'); @@ -564,7 +597,7 @@ describe('DatePicker', () => { fireEvent.change(input, { target: { value: 'garbage' } }); fireEvent.keyUp(input, { code: 'Enter' }); - expect(onErrorChange).toHaveBeenLastCalledWith(undefined); + expect(onValidityChange).toHaveBeenLastCalledWith({ valid: true }); }); it('renders no error presentation of its own, with no message and no aria-invalid', () => { @@ -573,7 +606,7 @@ describe('DatePicker', () => { const input = screen.getByPlaceholderText('Select date'); fireEvent.change(input, { target: { value: 'not a date' } }); - // Presentation is fully consumer-owned: `onErrorChange` lifted into + // Presentation is fully consumer-owned: `onValidityChange` lifted into // `Field`'s `error` prop renders the message and wires aria-invalid. expect(input.getAttribute('aria-invalid')).toBeNull(); expect(screen.queryByRole('alert')).toBeNull(); diff --git a/packages/react/components/calendar/date-picker.tsx b/packages/react/components/calendar/date-picker.tsx index aeb153c86..9045d28a9 100644 --- a/packages/react/components/calendar/date-picker.tsx +++ b/packages/react/components/calendar/date-picker.tsx @@ -8,6 +8,7 @@ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; import { useEffect, useRef, useState } from 'react'; import { PropsBase } from 'react-day-picker'; import { CalendarIcon } from '~/icons'; +import type { CalendarPreviewInputValidity } from '../calendar-preview/calendar-preview-input'; import { Input } from '../input'; import { InputProps } from '../input/input'; import { Popover } from '../popover'; @@ -27,6 +28,8 @@ dayjs.extend(isSameOrBefore); */ type DatePickerCalendarSlot = Omit & CalendarPropsExtended; +const VALID: CalendarPreviewInputValidity = { valid: true }; + export interface DatePickerSlotProps { input?: InputProps; calendar?: DatePickerCalendarSlot; @@ -48,14 +51,15 @@ export interface DatePickerProps { popoverProps?: PopoverContentProps; onSelect?: (date: Date) => void; /** - * Fires when the typed-input validation state changes: with a message when - * the typed text stops parsing as a valid in-bounds date, and with - * `undefined` when it becomes valid again (or the picker commits/closes). + * Fires when the typed-input validity changes: with `valid: false`, a + * `reason` and the message `'Invalid date'` when the typed text stops + * parsing as a valid in-bounds date, and with `{ valid: true }` when it + * becomes valid again (or the picker commits/closes). * DatePicker renders no error UI of its own, not even `aria-invalid`. - * Lift this into `Field`'s `error` prop (or your form library) to display - * it; `Field` also wires `aria-invalid` onto the input. + * Lift `message` into `Field`'s `error` prop (or your form library) to + * display it; `Field` also wires `aria-invalid` onto the input. */ - onErrorChange?: (error: string | undefined) => void; + onValidityChange?: (validity: CalendarPreviewInputValidity) => void; value?: Date; defaultValue?: Date; children?: @@ -74,7 +78,7 @@ export function DatePicker({ value: valueProp, defaultValue, onSelect = () => undefined, - onErrorChange, + onValidityChange, children, showCalendarIcon = true, timeZone @@ -98,11 +102,14 @@ export function DatePicker({ valueProp ?? defaultValue ); - const errorRef = useRef(undefined); + const validityRef = useRef(VALID); - function updateError(next: string | undefined) { - if (next !== errorRef.current) onErrorChange?.(next); - errorRef.current = next; + function updateValidity(next: CalendarPreviewInputValidity) { + const prev = validityRef.current; + validityRef.current = next; + if (next.valid !== prev.valid || next.reason !== prev.reason) { + onValidityChange?.(next); + } } // Sync only when controlled, since uncontrolled mode keeps its own state. @@ -157,9 +164,9 @@ export function DatePicker({ function closePicker() { popover.disengage(); const committedDate = selectedDateRef.current; - const hadError = errorRef.current !== undefined; + const hadError = !validityRef.current.valid; setInputValue(committedDate ? dayjs(committedDate).format(dateFormat) : ''); - updateError(undefined); + updateValidity(VALID); /* * Emit the committed Date directly. Going through * `dayjs(formattedString).toDate()` re-parses the formatted string without @@ -178,7 +185,7 @@ export function DatePicker({ // clicks the currently-selected day (deselect). Only forward defined // dates to consumer `onSelect`, which keeps the prop type narrow. if (day) onSelect(day); - updateError(undefined); + updateValidity(VALID); popover.disengage(); } @@ -218,9 +225,13 @@ export function DatePicker({ if (isValid) { setSelectedDate(date.toDate()); - updateError(undefined); + updateValidity(VALID); } else { - updateError('Invalid date'); + updateValidity({ + valid: false, + reason: isValidDate ? 'out-of-bounds' : 'unparseable', + message: 'Invalid date' + }); } } diff --git a/packages/react/components/command/command-dialog.tsx b/packages/react/components/command/command-dialog.tsx index b252d7444..e03521fa5 100644 --- a/packages/react/components/command/command-dialog.tsx +++ b/packages/react/components/command/command-dialog.tsx @@ -25,7 +25,6 @@ export const CommandDialogTrigger = forwardRef< CommandDialogTrigger.displayName = 'Command.DialogTrigger'; export interface CommandDialogContentProps extends DialogPrimitive.Popup.Props { - width?: string | number; /** Corner radius for this palette only. Overrides the theme's `radius`. */ radius?: Radius; } @@ -33,8 +32,6 @@ export interface CommandDialogContentProps extends DialogPrimitive.Popup.Props { export function CommandDialogContent({ className, children, - width, - style, radius, ...props }: CommandDialogContentProps) { @@ -64,7 +61,6 @@ export function CommandDialogContent({ radiusStyle({ radius }), className )} - style={{ width, ...style }} initialFocus={openType => { /* Runs before focus moves into the popup, so activeElement is still the element focused before opening. */ diff --git a/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx b/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx index ef8061735..73bf2f7f2 100644 --- a/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx +++ b/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx @@ -565,21 +565,25 @@ describe('FilterChip', () => { expect(icons(true)).toBe(1); }); - it('reports a typed error through onErrorChange', () => { - const onErrorChange = vi.fn(); + it('reports a typed error through onValidityChange', () => { + const onValidityChange = vi.fn(); render( ); const input = screen.getByPlaceholderText('Select date'); fireEvent.focus(input); fireEvent.change(input, { target: { value: 'not a date' } }); - expect(onErrorChange).toHaveBeenLastCalledWith('Invalid date'); + expect(onValidityChange).toHaveBeenLastCalledWith({ + valid: false, + reason: 'unparseable', + message: 'Invalid date' + }); fireEvent.change(input, { target: { value: '27 May 2026' } }); - expect(onErrorChange).toHaveBeenLastCalledWith(undefined); + expect(onValidityChange).toHaveBeenLastCalledWith({ valid: true }); }); it('keeps a typed error when a string value rerenders the chip', () => { @@ -592,7 +596,9 @@ describe('FilterChip', () => { label='Created' columnType={FilterType.date} value='2026-05-27' - calendarProps={{ onErrorChange: setError }} + calendarProps={{ + onValidityChange: validity => setError(validity.message) + }} /> ); diff --git a/packages/react/components/filter-chip/filter-chip.tsx b/packages/react/components/filter-chip/filter-chip.tsx index ee5b34a71..e944e8291 100644 --- a/packages/react/components/filter-chip/filter-chip.tsx +++ b/packages/react/components/filter-chip/filter-chip.tsx @@ -14,6 +14,7 @@ import { CalendarPreview, type CalendarPreviewContentProps, type CalendarPreviewInputProps, + type CalendarPreviewInputValidity, type CalendarPreviewProps } from '~/components/calendar-preview'; import { XIcon } from '~/icons'; @@ -49,7 +50,7 @@ const chip = cva(styles.chip, { export type FilterChipValue = FilterValueType | FilterValueType[] | Date; -/* The message `DatePicker` reports, so `onErrorChange` reads the same. */ +/* The message `DatePicker` reports, so `onValidityChange` reads the same. */ const INVALID_DATE = 'Invalid date'; /* A year the calendar cannot show leaves the field empty. */ @@ -89,8 +90,8 @@ export type FilterChipCalendarProps = Pick< * @default false */ showCalendarIcon?: boolean; - /** Called with a message when the typed date is invalid, and with `undefined` when it is valid again. */ - onErrorChange?: (error: string | undefined) => void; + /** Called when the typed date becomes invalid or valid again. */ + onValidityChange?: (validity: CalendarPreviewInputValidity) => void; }; export interface FilterChipProps @@ -163,7 +164,7 @@ export const FilterChip = ({ formatValue: formatDate, slotProps: dateSlotProps, showCalendarIcon = false, - onErrorChange, + onValidityChange, ...calendarRest } = calendarProps ?? {}; const { classNames: inputClassNames, ...inputProps } = @@ -309,9 +310,7 @@ export const FilterChip = ({ }} onValidityChange={validity => { inputProps.onValidityChange?.(validity); - onErrorChange?.( - validity.valid ? undefined : validity.message - ); + onValidityChange?.(validity); }} /> diff --git a/packages/react/components/flex/__tests__/flex.test.tsx b/packages/react/components/flex/__tests__/flex.test.tsx index 9a20c2409..470869d66 100644 --- a/packages/react/components/flex/__tests__/flex.test.tsx +++ b/packages/react/components/flex/__tests__/flex.test.tsx @@ -125,7 +125,7 @@ describe('Flex', () => { describe('Gap', () => { const numericGaps = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 ] as const; it.each(numericGaps)('renders gap %s correctly', gap => { diff --git a/packages/react/components/grid/__tests__/grid.test.tsx b/packages/react/components/grid/__tests__/grid.test.tsx index e4f264086..df7a86222 100644 --- a/packages/react/components/grid/__tests__/grid.test.tsx +++ b/packages/react/components/grid/__tests__/grid.test.tsx @@ -91,7 +91,7 @@ describe('Grid', () => { describe('Gap Properties', () => { const gaps = [ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 ] as const; it.each(gaps)('renders %s gap correctly', gap => { diff --git a/packages/react/components/search/__tests__/search.test.tsx b/packages/react/components/search/__tests__/search.test.tsx index beb941d91..58ea71fa5 100644 --- a/packages/react/components/search/__tests__/search.test.tsx +++ b/packages/react/components/search/__tests__/search.test.tsx @@ -2,7 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; import { Search } from '../search'; -import styles from '../search.module.css'; describe('Search', () => { describe('Basic Rendering', () => { @@ -194,20 +193,6 @@ describe('Search', () => { }); }); - describe('Width', () => { - it('defaults to 100% width', () => { - const { container } = render(); - const searchContainer = container.querySelector(`.${styles.container}`); - expect(searchContainer).toHaveStyle({ width: '100%' }); - }); - - it('sets custom width', () => { - const { container } = render(); - const searchContainer = container.querySelector(`.${styles.container}`); - expect(searchContainer).toHaveStyle({ width: '300px' }); - }); - }); - describe('Disabled State', () => { it('disables input when disabled', () => { render(); diff --git a/packages/react/components/search/search.tsx b/packages/react/components/search/search.tsx index fc0390275..51aa44683 100644 --- a/packages/react/components/search/search.tsx +++ b/packages/react/components/search/search.tsx @@ -9,7 +9,7 @@ import { InputProps } from '../input/input'; import styles from './search.module.css'; -export interface SearchProps extends InputProps { +export interface SearchProps extends Omit { showClearButton?: boolean; onClear?: ( event: MouseEvent | KeyboardEvent @@ -46,7 +46,6 @@ export function Search({ onClear, onKeyDown, value, - width = '100%', variant = 'default', ref, leadingIcon = , @@ -103,12 +102,7 @@ export function Search({ ) : undefined; return ( -
    +
    { }); describe('Styling', () => { - it('sets default border radius', () => { - const { container } = render(); - const skeleton = container.querySelector( - `.${styles.skeleton}` - ) as HTMLElement; - expect(skeleton.style.borderRadius).toBe('var(--rs-radius-2)'); - }); - - it('sets custom border radius as string', () => { - const { container } = render(); - const skeleton = container.querySelector( - `.${styles.skeleton}` - ) as HTMLElement; - expect(skeleton.style.borderRadius).toBe('10px'); - }); - - it('sets custom border radius as number', () => { - const { container } = render(); - const skeleton = container.querySelector( - `.${styles.skeleton}` - ) as HTMLElement; - expect(skeleton.style.borderRadius).toBe('5px'); + it('applies the radius class', () => { + const { container } = render(); + const skeleton = container.querySelector(`.${styles.skeleton}`); + expect(skeleton).toHaveClass(radiusClasses.full); }); it('applies custom styles', () => { diff --git a/packages/react/components/skeleton/skeleton.module.css b/packages/react/components/skeleton/skeleton.module.css index 2b4fd3458..d4471b3b0 100644 --- a/packages/react/components/skeleton/skeleton.module.css +++ b/packages/react/components/skeleton/skeleton.module.css @@ -4,6 +4,8 @@ --skeleton-duration: 1.5s; background-color: var(--skeleton-base-color); + --rs-radius-step: 4px; + border-radius: var(--rs-radius-2); overflow: hidden; position: relative; display: block; diff --git a/packages/react/components/skeleton/skeleton.tsx b/packages/react/components/skeleton/skeleton.tsx index b09698307..313813933 100644 --- a/packages/react/components/skeleton/skeleton.tsx +++ b/packages/react/components/skeleton/skeleton.tsx @@ -2,6 +2,7 @@ import { cx } from 'class-variance-authority'; import { CSSProperties, createContext, ReactNode, useContext } from 'react'; +import { type Radius, radiusStyle } from '../../shared/radius'; import styles from './skeleton.module.css'; export interface SkeletonProps { @@ -9,7 +10,8 @@ export interface SkeletonProps { highlightColor?: string; width?: string | number; height?: string | number; - borderRadius?: string | number; + /** Corner radius for this skeleton only. Overrides the theme's `radius`. */ + radius?: Radius; inline?: boolean; duration?: number; enableAnimation?: boolean; @@ -34,7 +36,7 @@ const SkeletonBase = (props: SkeletonProps) => { highlightColor, width, height = 'var(--rs-space-4)', - borderRadius = 'var(--rs-radius-2)', + radius, inline = false, duration = 1.5, count = 1, @@ -49,6 +51,7 @@ const SkeletonBase = (props: SkeletonProps) => { styles.skeleton, inline && styles.inline, enableAnimation && styles.animate, + radiusStyle({ radius }), className ); @@ -76,7 +79,6 @@ const SkeletonBase = (props: SkeletonProps) => { { width: width ?? defaultWidth, height, - borderRadius, '--skeleton-base-color': baseColor, '--skeleton-highlight-color': highlightColor, '--skeleton-duration': `${duration}s`, diff --git a/packages/react/components/toast/__tests__/toast.test.tsx b/packages/react/components/toast/__tests__/toast.test.tsx index 59a97129f..d22d81923 100644 --- a/packages/react/components/toast/__tests__/toast.test.tsx +++ b/packages/react/components/toast/__tests__/toast.test.tsx @@ -1,6 +1,7 @@ import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { radiusClasses } from '../../../shared/radius'; import { Toast, toastManager } from '../toast'; const renderWithProvider = ( @@ -19,6 +20,17 @@ describe('Toast', () => { renderWithProvider(); expect(screen.getByText('App content')).toBeInTheDocument(); }); + + it('applies radius to each toast', async () => { + renderWithProvider({ radius: 'full' }); + act(() => { + toastManager.add({ title: 'Rounded toast' }); + }); + const title = await screen.findByText('Rounded toast'); + expect(title.closest('[data-slot="toast"]')).toHaveClass( + radiusClasses.full + ); + }); }); describe('toastManager.add()', () => { diff --git a/packages/react/components/toast/toast-provider.tsx b/packages/react/components/toast/toast-provider.tsx index 013864a7b..0e7633957 100644 --- a/packages/react/components/toast/toast-provider.tsx +++ b/packages/react/components/toast/toast-provider.tsx @@ -2,6 +2,7 @@ import { Toast as ToastPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; +import type { Radius } from '../../shared/radius'; import { useThemeInjection } from '../theme/portal'; import styles from './toast.module.css'; import { @@ -25,6 +26,8 @@ export interface ToastProviderProps * @default "bottom-right" */ position?: ToastPosition; + /** Corner radius for these toasts only. Overrides the theme's `radius`. */ + radius?: Radius; /** * Toast manager instance. Defaults to the singleton exported as * `toastManager`. Provide a custom one created via @@ -33,15 +36,27 @@ export interface ToastProviderProps toastManager?: ToastManager; } -function ToastList({ position }: { position: ToastPosition }) { +function ToastList({ + position, + radius +}: { + position: ToastPosition; + radius?: Radius; +}) { const { toasts } = ToastPrimitive.useToastManager(); return toasts.map(toast => ( - + )); } export function ToastProvider({ position = 'bottom-right', + radius, toastManager = defaultToastManager, children, ...props @@ -67,7 +82,7 @@ export function ToastProvider({ )} data-slot='toast-viewport' > - + diff --git a/packages/react/components/toast/toast-root.tsx b/packages/react/components/toast/toast-root.tsx index 422414429..34e0f3f09 100644 --- a/packages/react/components/toast/toast-root.tsx +++ b/packages/react/components/toast/toast-root.tsx @@ -4,6 +4,7 @@ import { Toast as ToastPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; import type { ReactNode } from 'react'; import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon, XIcon } from '~/icons'; +import { type Radius, radiusStyle } from '../../shared/radius'; import { Button } from '../button'; import { Flex } from '../flex'; import { IconButton } from '../icon-button'; @@ -41,12 +42,15 @@ function getSwipeDirection(position: ToastPosition): SwipeDirection[] { export interface ToastRootProps extends ToastPrimitive.Root.Props { position?: ToastPosition; + /** Corner radius for this toast only. Overrides the theme's `radius`. */ + radius?: Radius; } export function ToastRoot({ toast, className, position = 'bottom-right', + radius, ...props }: ToastRootProps) { const swipeDirection = getSwipeDirection(position); @@ -67,7 +71,7 @@ export function ToastRoot({ return ( Components - Breadcrumb + Breadcrumb `, metadata: { nestable: false } }; diff --git a/packages/react/shared/gap/gap.module.css b/packages/react/shared/gap/gap.module.css index bb54133f7..fd036ca7f 100644 --- a/packages/react/shared/gap/gap.module.css +++ b/packages/react/shared/gap/gap.module.css @@ -1,3 +1,6 @@ +.gap-0 { + gap: 0; +} .gap-1 { gap: var(--rs-space-1); } @@ -50,6 +53,9 @@ gap: var(--rs-space-17); } +.column-gap-0 { + column-gap: 0; +} .column-gap-1 { column-gap: var(--rs-space-1); } @@ -102,6 +108,9 @@ column-gap: var(--rs-space-17); } +.row-gap-0 { + row-gap: 0; +} .row-gap-1 { row-gap: var(--rs-space-1); } diff --git a/packages/react/shared/gap/gap.ts b/packages/react/shared/gap/gap.ts index 47c86e62c..2af25d517 100644 --- a/packages/react/shared/gap/gap.ts +++ b/packages/react/shared/gap/gap.ts @@ -1,6 +1,7 @@ import styles from './gap.module.css'; export const gapVariants = { + 0: styles['gap-0'], 1: styles['gap-1'], 2: styles['gap-2'], 3: styles['gap-3'], @@ -21,6 +22,7 @@ export const gapVariants = { } as const; export const columnGapVariants = { + 0: styles['column-gap-0'], 1: styles['column-gap-1'], 2: styles['column-gap-2'], 3: styles['column-gap-3'], @@ -41,6 +43,7 @@ export const columnGapVariants = { } as const; export const rowGapVariants = { + 0: styles['row-gap-0'], 1: styles['row-gap-1'], 2: styles['row-gap-2'], 3: styles['row-gap-3'], From da024f28ecb343291df5ebd5337aa0cffcac1d66 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 20 Oct 2026 14:37:13 +0530 Subject: [PATCH 2/2] refactor: [filter-chip, calendar] keep one typed-date validity callback - Remove FilterChip calendarProps.onValidityChange. It repeated calendarProps.slotProps.input.onValidityChange. - Shorten the DatePicker onValidityChange JSDoc and move its usage notes to the Calendar page. - Set the docs search width in CSS instead of an inline style. - List Skeleton and Toast.Provider under per-component radius, and DataView.Search in the upgrading notes. --- apps/www/src/components/docs/search.module.css | 1 + apps/www/src/components/docs/search.tsx | 5 +---- .../src/content/docs/(overview)/upgrading.mdx | 17 ++++++++++------- .../content/docs/components/calendar/index.mdx | 2 ++ .../content/docs/components/calendar/props.ts | 11 +---------- .../docs/components/filter-chip/props.ts | 8 +------- .../src/content/docs/theme/overview/index.mdx | 2 +- .../react/components/calendar/date-picker.tsx | 10 +--------- .../filter-chip/__tests__/filter-chip.test.tsx | 8 ++++++-- .../components/filter-chip/filter-chip.tsx | 8 -------- 10 files changed, 24 insertions(+), 48 deletions(-) diff --git a/apps/www/src/components/docs/search.module.css b/apps/www/src/components/docs/search.module.css index 4b44bd1e0..1e16b47dc 100644 --- a/apps/www/src/components/docs/search.module.css +++ b/apps/www/src/components/docs/search.module.css @@ -1,4 +1,5 @@ .searchContainer { + width: 512px; border-radius: var(--rs-radius-5); overflow: hidden; } diff --git a/apps/www/src/components/docs/search.tsx b/apps/www/src/components/docs/search.tsx index 7f4c1ff50..7c6b733cc 100644 --- a/apps/www/src/components/docs/search.tsx +++ b/apps/www/src/components/docs/search.tsx @@ -198,10 +198,7 @@ export default function DocsSearch({ pageTree }: { pageTree: Root }) { > - + ### Controlled diff --git a/apps/www/src/content/docs/components/calendar/props.ts b/apps/www/src/content/docs/components/calendar/props.ts index cc29b83c1..de1296681 100644 --- a/apps/www/src/content/docs/components/calendar/props.ts +++ b/apps/www/src/content/docs/components/calendar/props.ts @@ -249,16 +249,7 @@ export interface DatePickerProps { /** Callback function when date is selected */ onSelect?: (date: Date) => void; - /** - * Fires when the typed-input validity changes. An invalid validity has a - * `reason` (`unparseable`, or `out-of-bounds` outside `startMonth` and - * `endMonth`) and the message `'Invalid date'`. It is `{ valid: true }` when - * the text is valid again or the picker commits or closes. - * DatePicker renders no error UI of its own, not even `aria-invalid`. - * Lift `message` into `Field`'s `error` prop (or your form library) to - * display it; `Field` also wires `aria-invalid` onto the input. - * @example onValidityChange={({ message }) => setError(message)} - */ + /** Called when the typed date becomes invalid or valid again. */ onValidityChange?: (validity: { valid: boolean; reason?: 'unparseable' | 'out-of-bounds' | 'unavailable' | 'out-of-order'; diff --git a/apps/www/src/content/docs/components/filter-chip/props.ts b/apps/www/src/content/docs/components/filter-chip/props.ts index 9354b53f3..bbd8d2d6b 100644 --- a/apps/www/src/content/docs/components/filter-chip/props.ts +++ b/apps/www/src/content/docs/components/filter-chip/props.ts @@ -67,7 +67,7 @@ export interface FilterChipProps { yearRange?: { from: number; to: number }; defaultMonth?: Date; today?: Date; - /** Props for the date input (`CalendarPreview.Input`) and its popup (`CalendarPreview.Content`). `input.disabled` and `input.readOnly` also apply to the calendar. */ + /** Props for the date input (`CalendarPreview.Input`) and its popup (`CalendarPreview.Content`). `input.disabled` and `input.readOnly` also apply to the calendar. `input.onValidityChange` reports typed-date errors. */ slotProps?: { input?: Omit; popover?: Omit; @@ -77,12 +77,6 @@ export interface FilterChipProps { * @default false */ showCalendarIcon?: boolean; - /** Called when the typed date becomes invalid or valid again. Same as `onValidityChange` on `CalendarPreview.Input`. */ - onValidityChange?: (validity: { - valid: boolean; - reason?: 'unparseable' | 'out-of-bounds' | 'unavailable' | 'out-of-order'; - message?: string; - }) => void; }; /** Additional CSS class names */ diff --git a/apps/www/src/content/docs/theme/overview/index.mdx b/apps/www/src/content/docs/theme/overview/index.mdx index b53f21d28..f937bbdda 100644 --- a/apps/www/src/content/docs/theme/overview/index.mdx +++ b/apps/www/src/content/docs/theme/overview/index.mdx @@ -234,7 +234,7 @@ Components accept a `radius` prop with the theme's five values. It affects only -Available on `Button`, `IconButton`, `Badge`, `Callout`, `Chip`, `Input`, `TextArea`, `Image`, `Avatar`, and on the portalled parts `Dialog.Content`, `AlertDialog.Content`, `Drawer.Content`, `Popover.Content`, `Menu.Content`, `ContextMenu.Content`, `Select.Content`, `Combobox.Content`, `Tooltip.Content`, `PreviewCard.Content`, `Command.DialogContent` and `Tour.Content`. `Menu.SubmenuContent` and `ContextMenu.SubContent` take it too. It goes on the portalled part, not the root: ``. +Available on `Button`, `IconButton`, `Badge`, `Callout`, `Chip`, `Input`, `TextArea`, `Image`, `Avatar`, `Skeleton`, and on the portalled parts `Dialog.Content`, `AlertDialog.Content`, `Drawer.Content`, `Popover.Content`, `Menu.Content`, `ContextMenu.Content`, `Select.Content`, `Combobox.Content`, `Tooltip.Content`, `PreviewCard.Content`, `Command.DialogContent` and `Tour.Content`. `Menu.SubmenuContent` and `ContextMenu.SubContent` take it too. It goes on the portalled part, not the root: ``. `Toast.Provider` takes it for every toast it renders. ## Tokens diff --git a/packages/react/components/calendar/date-picker.tsx b/packages/react/components/calendar/date-picker.tsx index 9045d28a9..d4b73219b 100644 --- a/packages/react/components/calendar/date-picker.tsx +++ b/packages/react/components/calendar/date-picker.tsx @@ -50,15 +50,7 @@ export interface DatePickerProps { /** @deprecated Use `slotProps.popover` instead. */ popoverProps?: PopoverContentProps; onSelect?: (date: Date) => void; - /** - * Fires when the typed-input validity changes: with `valid: false`, a - * `reason` and the message `'Invalid date'` when the typed text stops - * parsing as a valid in-bounds date, and with `{ valid: true }` when it - * becomes valid again (or the picker commits/closes). - * DatePicker renders no error UI of its own, not even `aria-invalid`. - * Lift `message` into `Field`'s `error` prop (or your form library) to - * display it; `Field` also wires `aria-invalid` onto the input. - */ + /** Called when the typed date becomes invalid or valid again. */ onValidityChange?: (validity: CalendarPreviewInputValidity) => void; value?: Date; defaultValue?: Date; diff --git a/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx b/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx index 73bf2f7f2..c82920a60 100644 --- a/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx +++ b/packages/react/components/filter-chip/__tests__/filter-chip.test.tsx @@ -571,7 +571,7 @@ describe('FilterChip', () => { ); const input = screen.getByPlaceholderText('Select date'); @@ -597,7 +597,11 @@ describe('FilterChip', () => { columnType={FilterType.date} value='2026-05-27' calendarProps={{ - onValidityChange: validity => setError(validity.message) + slotProps: { + input: { + onValidityChange: validity => setError(validity.message) + } + } }} /> diff --git a/packages/react/components/filter-chip/filter-chip.tsx b/packages/react/components/filter-chip/filter-chip.tsx index e944e8291..9e93e1c48 100644 --- a/packages/react/components/filter-chip/filter-chip.tsx +++ b/packages/react/components/filter-chip/filter-chip.tsx @@ -14,7 +14,6 @@ import { CalendarPreview, type CalendarPreviewContentProps, type CalendarPreviewInputProps, - type CalendarPreviewInputValidity, type CalendarPreviewProps } from '~/components/calendar-preview'; import { XIcon } from '~/icons'; @@ -90,8 +89,6 @@ export type FilterChipCalendarProps = Pick< * @default false */ showCalendarIcon?: boolean; - /** Called when the typed date becomes invalid or valid again. */ - onValidityChange?: (validity: CalendarPreviewInputValidity) => void; }; export interface FilterChipProps @@ -164,7 +161,6 @@ export const FilterChip = ({ formatValue: formatDate, slotProps: dateSlotProps, showCalendarIcon = false, - onValidityChange, ...calendarRest } = calendarProps ?? {}; const { classNames: inputClassNames, ...inputProps } = @@ -308,10 +304,6 @@ export const FilterChip = ({ unavailable: INVALID_DATE, ...inputProps.errorMessages }} - onValidityChange={validity => { - inputProps.onValidityChange?.(validity); - onValidityChange?.(validity); - }} />