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.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 e66779c03..7c6b733cc 100644 --- a/apps/www/src/components/docs/search.tsx +++ b/apps/www/src/components/docs/search.tsx @@ -198,7 +198,7 @@ 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` and `CalendarPreview.Input` report the validity of typed text with +`onValidityChange`. `FilterChip` takes it on `calendarProps.slotProps.input`, +which is its `CalendarPreview.Input`. The callback 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/index.mdx b/apps/www/src/content/docs/components/calendar/index.mdx index c642b2afe..50f3af764 100644 --- a/apps/www/src/content/docs/components/calendar/index.mdx +++ b/apps/www/src/content/docs/components/calendar/index.mdx @@ -68,6 +68,8 @@ Selects a date range across two inputs. The grid uses a state machine that branc Single-date selection with a typable input. The popover opens on focus; pressing Enter, blurring, or clicking outside commits the value and fires `onSelect`. +`onValidityChange` reports typed text that is not a date (`reason: 'unparseable'`) or is outside `startMonth` and `endMonth` (`reason: 'out-of-bounds'`), with the message `'Invalid date'`. It reports `{ valid: true }` when the text is valid again, or when the picker commits or closes. DatePicker shows no error and sets no `aria-invalid`. To show the error, pass `message` to the `error` prop of `Field`, as in the With Field example. `Field` also sets `aria-invalid` on the input. + ### Controlled diff --git a/apps/www/src/content/docs/components/calendar/props.ts b/apps/www/src/content/docs/components/calendar/props.ts index e3f047615..de1296681 100644 --- a/apps/www/src/content/docs/components/calendar/props.ts +++ b/apps/www/src/content/docs/components/calendar/props.ts @@ -249,15 +249,12 @@ export interface DatePickerProps { /** Callback function when date is selected */ 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. - */ - onErrorChange?: (error: string | undefined) => void; + /** Called when the typed date becomes invalid or valid again. */ + 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..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,8 +77,6 @@ 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; }; /** 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/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/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..d4b73219b 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; @@ -47,15 +50,8 @@ export interface DatePickerProps { /** @deprecated Use `slotProps.popover` instead. */ 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). - * 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. - */ - onErrorChange?: (error: string | undefined) => void; + /** Called when the typed date becomes invalid or valid again. */ + onValidityChange?: (validity: CalendarPreviewInputValidity) => void; value?: Date; defaultValue?: Date; children?: @@ -74,7 +70,7 @@ export function DatePicker({ value: valueProp, defaultValue, onSelect = () => undefined, - onErrorChange, + onValidityChange, children, showCalendarIcon = true, timeZone @@ -98,11 +94,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 +156,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 +177,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 +217,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..c82920a60 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,13 @@ describe('FilterChip', () => { label='Created' columnType={FilterType.date} value='2026-05-27' - calendarProps={{ onErrorChange: setError }} + calendarProps={{ + 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 ee5b34a71..9e93e1c48 100644 --- a/packages/react/components/filter-chip/filter-chip.tsx +++ b/packages/react/components/filter-chip/filter-chip.tsx @@ -49,7 +49,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 +89,6 @@ 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; }; export interface FilterChipProps @@ -163,7 +161,6 @@ export const FilterChip = ({ formatValue: formatDate, slotProps: dateSlotProps, showCalendarIcon = false, - onErrorChange, ...calendarRest } = calendarProps ?? {}; const { classNames: inputClassNames, ...inputProps } = @@ -307,12 +304,6 @@ export const FilterChip = ({ unavailable: INVALID_DATE, ...inputProps.errorMessages }} - onValidityChange={validity => { - inputProps.onValidityChange?.(validity); - onErrorChange?.( - validity.valid ? undefined : validity.message - ); - }} /> 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'],