From 09c4e2c17ed4441e4e0c4b0eacce322d589aeefd Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 18 Mar 2026 01:09:44 +0530 Subject: [PATCH 01/11] feat: migrate to React 19 --- .../accordion/accordion-content.tsx | 18 +-- .../components/accordion/accordion-item.tsx | 13 +- .../components/accordion/accordion-root.tsx | 98 ++++++------- .../accordion/accordion-trigger.tsx | 13 +- .../alert-dialog/alert-dialog-content.tsx | 80 +++++------ .../alert-dialog/alert-dialog-misc.tsx | 68 ++++----- .../raystack/components/amount/amount.tsx | 132 +++++++++--------- .../announcement-bar/announcement-bar.tsx | 10 +- .../raystack/components/avatar/avatar.tsx | 117 ++++++++-------- packages/raystack/components/badge/badge.tsx | 52 +++---- .../raystack/components/box/box.module.css | 3 - packages/raystack/components/box/box.tsx | 24 +--- .../components/breadcrumb/breadcrumb-item.tsx | 127 ++++++++--------- .../components/breadcrumb/breadcrumb-misc.tsx | 80 ++++------- .../components/breadcrumb/breadcrumb-root.tsx | 33 +++-- .../raystack/components/button/button.tsx | 123 ++++++++-------- .../raystack/components/calendar/calendar.tsx | 2 + .../components/calendar/date-picker.tsx | 14 +- .../components/calendar/range-picker.tsx | 8 +- 19 files changed, 468 insertions(+), 547 deletions(-) delete mode 100644 packages/raystack/components/box/box.module.css diff --git a/packages/raystack/components/accordion/accordion-content.tsx b/packages/raystack/components/accordion/accordion-content.tsx index 5dc7183bc..088048717 100644 --- a/packages/raystack/components/accordion/accordion-content.tsx +++ b/packages/raystack/components/accordion/accordion-content.tsx @@ -2,22 +2,18 @@ import { Accordion as AccordionPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { ElementRef, forwardRef } from 'react'; import styles from './accordion.module.css'; -export const AccordionContent = forwardRef< - ElementRef, - AccordionPrimitive.Panel.Props ->(({ className, children, ...props }, ref) => ( - +export const AccordionContent = ({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props) => ( +
{children}
-)); +); AccordionContent.displayName = 'Accordion.Content'; diff --git a/packages/raystack/components/accordion/accordion-item.tsx b/packages/raystack/components/accordion/accordion-item.tsx index 3b487fa5c..0b937b8c7 100644 --- a/packages/raystack/components/accordion/accordion-item.tsx +++ b/packages/raystack/components/accordion/accordion-item.tsx @@ -2,20 +2,19 @@ import { Accordion as AccordionPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { ElementRef, forwardRef } from 'react'; import styles from './accordion.module.css'; -export const AccordionItem = forwardRef< - ElementRef, - AccordionPrimitive.Item.Props ->(({ className, children, ...props }, ref) => ( +export const AccordionItem = ({ + className, + children, + ...props +}: AccordionPrimitive.Item.Props) => ( {children} -)); +); AccordionItem.displayName = 'Accordion.Item'; diff --git a/packages/raystack/components/accordion/accordion-root.tsx b/packages/raystack/components/accordion/accordion-root.tsx index be7030010..6544de9cd 100644 --- a/packages/raystack/components/accordion/accordion-root.tsx +++ b/packages/raystack/components/accordion/accordion-root.tsx @@ -2,7 +2,6 @@ import { Accordion as AccordionPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { ElementRef, forwardRef } from 'react'; import styles from './accordion.module.css'; type AccordionSingleProps = Omit< @@ -27,61 +26,52 @@ type AccordionMultipleProps = Omit< export type AccordionRootProps = AccordionSingleProps | AccordionMultipleProps; -export const AccordionRoot = forwardRef< - ElementRef, - AccordionRootProps ->( - ( - { - className, - multiple = false, - value, - defaultValue, - onValueChange, - ...rest - }, - ref - ) => { - // Convert value to array format for Base UI - const baseValue = value - ? Array.isArray(value) - ? value - : [value] - : undefined; +export const AccordionRoot = ({ + className, + multiple = false, + value, + defaultValue, + onValueChange, + ...rest +}: AccordionRootProps) => { + // Convert value to array format for Base UI + const baseValue = value + ? Array.isArray(value) + ? value + : [value] + : undefined; - const baseDefaultValue = defaultValue - ? Array.isArray(defaultValue) - ? defaultValue - : [defaultValue] - : undefined; + const baseDefaultValue = defaultValue + ? Array.isArray(defaultValue) + ? defaultValue + : [defaultValue] + : undefined; - const handleValueChange = ( - newValue: string[], - eventDetails: AccordionPrimitive.Root.ChangeEventDetails - ) => { - if (onValueChange) { - if (multiple) { - (onValueChange as (value: string[]) => void)(newValue); - } else { - (onValueChange as (value: string | undefined) => void)( - newValue[0] || undefined - ); - } + const handleValueChange = ( + newValue: string[], + eventDetails: AccordionPrimitive.Root.ChangeEventDetails + ) => { + if (onValueChange) { + if (multiple) { + (onValueChange as (value: string[]) => void)(newValue); + } else { + (onValueChange as (value: string | undefined) => void)( + newValue[0] || undefined + ); } - }; + } + }; - return ( - - ); - } -); + return ( + + ); +}; -AccordionRoot.displayName = 'Accordion.Root'; +AccordionRoot.displayName = 'Accordion'; diff --git a/packages/raystack/components/accordion/accordion-trigger.tsx b/packages/raystack/components/accordion/accordion-trigger.tsx index ef50a0ad3..c52735e4d 100644 --- a/packages/raystack/components/accordion/accordion-trigger.tsx +++ b/packages/raystack/components/accordion/accordion-trigger.tsx @@ -3,16 +3,15 @@ import { Accordion as AccordionPrimitive } from '@base-ui/react'; import { ChevronDownIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import { ElementRef, forwardRef } from 'react'; import styles from './accordion.module.css'; -export const AccordionTrigger = forwardRef< - ElementRef, - AccordionPrimitive.Trigger.Props ->(({ className, children, ...props }, ref) => ( +export const AccordionTrigger = ({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props) => ( @@ -20,6 +19,6 @@ export const AccordionTrigger = forwardRef< -)); +); AccordionTrigger.displayName = 'Accordion.Trigger'; diff --git a/packages/raystack/components/alert-dialog/alert-dialog-content.tsx b/packages/raystack/components/alert-dialog/alert-dialog-content.tsx index e60acfdda..824cbb154 100644 --- a/packages/raystack/components/alert-dialog/alert-dialog-content.tsx +++ b/packages/raystack/components/alert-dialog/alert-dialog-content.tsx @@ -2,7 +2,6 @@ import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { type ElementRef, forwardRef } from 'react'; import styles from '../dialog/dialog.module.css'; import { CloseButton } from './alert-dialog-misc'; @@ -18,51 +17,42 @@ export interface AlertDialogContentProps showNestedAnimation?: boolean; } -export const AlertDialogContent = forwardRef< - ElementRef, - AlertDialogContentProps ->( - ( - { - className, - children, - showCloseButton = true, - overlay, - width, - style, - showNestedAnimation = true, - ...props - }, - ref - ) => { - return ( - - { + return ( + + + + - - - {children} - {showCloseButton && } - - - - ); - } -); + style={{ width, ...style }} + {...props} + > + {children} + {showCloseButton && } + + + + ); +}; AlertDialogContent.displayName = 'AlertDialog.Content'; diff --git a/packages/raystack/components/alert-dialog/alert-dialog-misc.tsx b/packages/raystack/components/alert-dialog/alert-dialog-misc.tsx index f0fe92e4b..d242a196f 100644 --- a/packages/raystack/components/alert-dialog/alert-dialog-misc.tsx +++ b/packages/raystack/components/alert-dialog/alert-dialog-misc.tsx @@ -3,62 +3,58 @@ import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react'; import { Cross1Icon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import { ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react'; +import { type ComponentProps } from 'react'; import styles from '../dialog/dialog.module.css'; import { Flex } from '../flex'; -export const AlertDialogHeader = forwardRef< - ElementRef, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( +export const AlertDialogHeader = ({ + className, + ...props +}: ComponentProps) => ( -)); +); AlertDialogHeader.displayName = 'AlertDialog.Header'; -export const AlertDialogFooter = forwardRef< - ElementRef, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( +export const AlertDialogFooter = ({ + className, + ...props +}: ComponentProps) => ( -)); +); AlertDialogFooter.displayName = 'AlertDialog.Footer'; -export const AlertDialogBody = forwardRef< - ElementRef, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( +export const AlertDialogBody = ({ + className, + ...props +}: ComponentProps) => ( -)); +); AlertDialogBody.displayName = 'AlertDialog.Body'; -export const CloseButton = forwardRef< - ElementRef, - AlertDialogPrimitive.Close.Props ->(({ className, ...props }, ref) => { +export const CloseButton = ({ + className, + ...props +}: AlertDialogPrimitive.Close.Props) => { return ( ); -}); +}; CloseButton.displayName = 'AlertDialog.CloseButton'; -export const AlertDialogTitle = forwardRef< - ElementRef, - AlertDialogPrimitive.Title.Props ->(({ className, ...props }, ref) => { +export const AlertDialogTitle = ({ + className, + ...props +}: AlertDialogPrimitive.Title.Props) => { return ( ); -}); +}; AlertDialogTitle.displayName = 'AlertDialog.Title'; -export const AlertDialogDescription = forwardRef< - ElementRef, - AlertDialogPrimitive.Description.Props ->(({ className, ...props }, ref) => { +export const AlertDialogDescription = ({ + className, + ...props +}: AlertDialogPrimitive.Description.Props) => { return ( ); -}); +}; AlertDialogDescription.displayName = 'AlertDialog.Description'; diff --git a/packages/raystack/components/amount/amount.tsx b/packages/raystack/components/amount/amount.tsx index 2f6964e1c..3815e737e 100644 --- a/packages/raystack/components/amount/amount.tsx +++ b/packages/raystack/components/amount/amount.tsx @@ -1,6 +1,6 @@ -import { forwardRef } from 'react'; +import { type ComponentProps } from 'react'; -export interface AmountProps { +export interface AmountProps extends ComponentProps<'span'> { /** * The monetary value to display * For large numbers (> 2^53), pass the value as string to maintain precision @@ -141,74 +141,68 @@ function isValidCurrency(currency: string): boolean { * * ``` */ -export const Amount = forwardRef( - ( - { - value = 0, - currency = 'USD', - locale = 'en-US', - hideDecimals = false, - currencyDisplay = 'symbol', - minimumFractionDigits, - maximumFractionDigits, - groupDigits = true, - valueInMinorUnits = true - }, - ref - ) => { - try { - if ( - typeof value === 'number' && - Math.abs(value) > Number.MAX_SAFE_INTEGER - ) { - console.warn( - `Warning: The number ${value} exceeds JavaScript's safe integer limit (${Number.MAX_SAFE_INTEGER}). ` + - 'For large numbers, pass the value as a string to maintain precision.' - ); - } - - const validCurrency = isValidCurrency(currency) ? currency : 'USD'; - if (validCurrency !== currency) { - console.warn( - `Invalid currency code: ${currency}. Falling back to USD.` - ); - } - - const decimals = getCurrencyDecimals(validCurrency); - - // Handle minor units - use string manipulation for strings and Math.pow for numbers - const baseValue = - valueInMinorUnits && decimals > 0 - ? typeof value === 'string' - ? value.slice(0, -decimals) + '.' + value.slice(-decimals) - : value / Math.pow(10, decimals) - : value; - - // Remove decimals if hideDecimals is true - handle string and number separately - // Note: Not all numbers passed is converted to string as methods like Math.trunc - // or toString cannot handle large numbers thus, we need to handle it separately (large numbers passed in value throws console warning). - const finalBaseValue = hideDecimals - ? typeof baseValue === 'string' - ? baseValue.split('.')[0] - : Math.trunc(baseValue) - : baseValue; - - const formattedValue = new Intl.NumberFormat(locale, { - style: 'currency' as const, - currency: validCurrency.toUpperCase(), - currencyDisplay, - minimumFractionDigits: hideDecimals ? 0 : minimumFractionDigits, - maximumFractionDigits: hideDecimals ? 0 : maximumFractionDigits, - useGrouping: groupDigits - // @ts-ignore - Handling large numbers as string or number, so we need to pass the value as string or number. - }).format(finalBaseValue); - - return {formattedValue}; - } catch (error) { - console.error('Error formatting amount:', error); - return {value}; +export const Amount = ({ + value = 0, + currency = 'USD', + locale = 'en-US', + hideDecimals = false, + currencyDisplay = 'symbol', + minimumFractionDigits, + maximumFractionDigits, + groupDigits = true, + valueInMinorUnits = true, + ...props +}: AmountProps) => { + try { + if ( + typeof value === 'number' && + Math.abs(value) > Number.MAX_SAFE_INTEGER + ) { + console.warn( + `Warning: The number ${value} exceeds JavaScript's safe integer limit (${Number.MAX_SAFE_INTEGER}). ` + + 'For large numbers, pass the value as a string to maintain precision.' + ); + } + + const validCurrency = isValidCurrency(currency) ? currency : 'USD'; + if (validCurrency !== currency) { + console.warn(`Invalid currency code: ${currency}. Falling back to USD.`); } + + const decimals = getCurrencyDecimals(validCurrency); + + // Handle minor units - use string manipulation for strings and Math.pow for numbers + const baseValue = + valueInMinorUnits && decimals > 0 + ? typeof value === 'string' + ? value.slice(0, -decimals) + '.' + value.slice(-decimals) + : value / Math.pow(10, decimals) + : value; + + // Remove decimals if hideDecimals is true - handle string and number separately + // Note: Not all numbers passed is converted to string as methods like Math.trunc + // or toString cannot handle large numbers thus, we need to handle it separately (large numbers passed in value throws console warning). + const finalBaseValue = hideDecimals + ? typeof baseValue === 'string' + ? baseValue.split('.')[0] + : Math.trunc(baseValue) + : baseValue; + + const formattedValue = new Intl.NumberFormat(locale, { + style: 'currency' as const, + currency: validCurrency.toUpperCase(), + currencyDisplay, + minimumFractionDigits: hideDecimals ? 0 : minimumFractionDigits, + maximumFractionDigits: hideDecimals ? 0 : maximumFractionDigits, + useGrouping: groupDigits + // @ts-ignore - Handling large numbers as string or number, so we need to pass the value as string or number. + }).format(finalBaseValue); + + return {formattedValue}; + } catch (error) { + console.error('Error formatting amount:', error); + return {value}; } -); +}; Amount.displayName = 'Amount'; diff --git a/packages/raystack/components/announcement-bar/announcement-bar.tsx b/packages/raystack/components/announcement-bar/announcement-bar.tsx index 817ef3d36..302c73418 100644 --- a/packages/raystack/components/announcement-bar/announcement-bar.tsx +++ b/packages/raystack/components/announcement-bar/announcement-bar.tsx @@ -1,13 +1,13 @@ 'use client'; -import { type VariantProps, cva } from 'class-variance-authority'; +import { cva, type VariantProps } from 'class-variance-authority'; import { ReactNode } from 'react'; import { Flex } from '../flex'; import { Text } from '../text'; import styles from './announcement-bar.module.css'; -const announementBar = cva(styles['announcement-bar'], { +const announcementBar = cva(styles['announcement-bar'], { variants: { variant: { gradient: styles['announcement-bar-gradient'], @@ -20,7 +20,7 @@ const announementBar = cva(styles['announcement-bar'], { } }); -type AnnouncementBarProps = VariantProps & { +type AnnouncementBarProps = VariantProps & { leadingIcon?: ReactNode; className?: string; text: string; @@ -41,7 +41,7 @@ export const AnnouncementBar = ({ }: AnnouncementBarProps) => { return ( ); }; + +AnnouncementBar.displayName = 'AnnouncementBar'; diff --git a/packages/raystack/components/avatar/avatar.tsx b/packages/raystack/components/avatar/avatar.tsx index ee0a7170c..48dde77eb 100644 --- a/packages/raystack/components/avatar/avatar.tsx +++ b/packages/raystack/components/avatar/avatar.tsx @@ -1,12 +1,6 @@ import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar'; import { cva, cx, VariantProps } from 'class-variance-authority'; -import { - ComponentPropsWithoutRef, - forwardRef, - isValidElement, - ReactElement, - ReactNode -} from 'react'; +import { ComponentProps, isValidElement, ReactElement, ReactNode } from 'react'; import styles from './avatar.module.css'; import { AVATAR_COLORS } from './utils'; @@ -160,64 +154,73 @@ export interface AvatarProps className?: string; } -const AvatarRoot = forwardRef( - ( - { className, alt, src, fallback, size, radius, variant, color, ...props }, - ref - ) => ( - - - - {fallback} - - - ) +const AvatarRoot = ({ + className, + alt, + src, + fallback, + size, + radius, + variant, + color, + ...props +}: AvatarProps) => ( + + + + {fallback} + + ); -AvatarRoot.displayName = AvatarPrimitive.Root.displayName; +AvatarRoot.displayName = 'Avatar'; export const Avatar = AvatarRoot; -export interface AvatarGroupProps extends ComponentPropsWithoutRef<'div'> { +export interface AvatarGroupProps extends ComponentProps<'div'> { children: React.ReactElement[]; max?: number; } -export const AvatarGroup = forwardRef( - ({ children, max, className, ...props }, ref) => { - const avatars = max ? children.slice(0, max) : children; - const count = max && children.length > max ? children.length - max : 0; +export const AvatarGroup = ({ + children, + max, + className, + ...props +}: AvatarGroupProps) => { + const avatars = max ? children.slice(0, max) : children; + const count = max && children.length > max ? children.length - max : 0; - // Overflow avatar matches the first avatar styling - const firstAvatarProps = getAvatarProps(avatars[0]); + // Overflow avatar matches the first avatar styling + const firstAvatarProps = getAvatarProps(avatars[0]); - return ( -
- {avatars.map((avatar, index) => ( -
- {avatar} -
- ))} - {count > 0 && ( -
- -
- )} -
- ); - } -); + return ( +
+ {avatars.map((avatar, index) => ( +
+ {avatar} +
+ ))} + {count > 0 && ( +
+ +
+ )} +
+ ); +}; + +AvatarGroup.displayName = 'Avatar.Group'; diff --git a/packages/raystack/components/badge/badge.tsx b/packages/raystack/components/badge/badge.tsx index 613f545d0..222e0d9cd 100644 --- a/packages/raystack/components/badge/badge.tsx +++ b/packages/raystack/components/badge/badge.tsx @@ -1,47 +1,49 @@ -import { cva, type VariantProps } from "class-variance-authority"; -import { ReactNode } from "react"; +import { cva, type VariantProps } from 'class-variance-authority'; +import { ComponentProps, ReactNode } from 'react'; -import styles from "./badge.module.css"; +import styles from './badge.module.css'; const badge = cva(styles['badge'], { variants: { variant: { - accent: styles["badge-accent"], - warning: styles["badge-warning"], - danger: styles["badge-danger"], - success: styles["badge-success"], - neutral: styles["badge-neutral"], - gradient: styles["badge-gradient"] + accent: styles['badge-accent'], + warning: styles['badge-warning'], + danger: styles['badge-danger'], + success: styles['badge-success'], + neutral: styles['badge-neutral'], + gradient: styles['badge-gradient'] }, size: { - micro: styles["badge-micro"], - small: styles["badge-small"], - regular: styles["badge-regular"], + micro: styles['badge-micro'], + small: styles['badge-small'], + regular: styles['badge-regular'] }, defaultVariants: { - variant: "accent", - size: "small" + variant: 'accent', + size: 'small' } - }, + } }); -type BadgeProps = VariantProps & { - icon?: ReactNode; - children: ReactNode; - className?: string; - screenReaderText?: string; -} +type BadgeProps = VariantProps & + ComponentProps<'span'> & { + icon?: ReactNode; + children: ReactNode; + className?: string; + screenReaderText?: string; + }; -export const Badge = ({ +export const Badge = ({ variant = 'accent', size = 'small', icon, children, className, - screenReaderText + screenReaderText, + ...props }: BadgeProps) => { return ( - + {icon && {icon}} {screenReaderText && ( {screenReaderText} @@ -51,4 +53,4 @@ export const Badge = ({ ); }; -Badge.displayName = "Badge"; +Badge.displayName = 'Badge'; diff --git a/packages/raystack/components/box/box.module.css b/packages/raystack/components/box/box.module.css deleted file mode 100644 index 9f946dcbe..000000000 --- a/packages/raystack/components/box/box.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.box { - box-sizing: border-box; -} \ No newline at end of file diff --git a/packages/raystack/components/box/box.tsx b/packages/raystack/components/box/box.tsx index 9618584c3..87d923193 100644 --- a/packages/raystack/components/box/box.tsx +++ b/packages/raystack/components/box/box.tsx @@ -1,23 +1,7 @@ -import { cva, VariantProps } from "class-variance-authority"; -import { HTMLAttributes, PropsWithChildren } from "react"; +import { ComponentProps } from 'react'; -import styles from "./box.module.css"; - -// Note: This is the old component just copied in v1 folder as it is used in v1. - -const box = cva(styles.box); - -type BoxProps = PropsWithChildren> & - HTMLAttributes & { - ref?: React.RefObject; - }; - -export function Box({ children, className, ref, ...props }: BoxProps) { - return ( -
- {children} -
- ); +export function Box(props: ComponentProps<'div'>) { + return
; } -Box.displayName = "Box"; +Box.displayName = 'Box'; diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 36f00e6f9..18c3d82eb 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -3,9 +3,8 @@ import { ChevronDownIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; import React, { + ComponentProps, cloneElement, - forwardRef, - HTMLAttributes, ReactElement, ReactNode } from 'react'; @@ -18,80 +17,72 @@ export interface BreadcrumbDropdownItem { onClick?: React.MouseEventHandler; } -export interface BreadcrumbItemProps extends HTMLAttributes { +export interface BreadcrumbItemProps extends ComponentProps<'a'> { leadingIcon?: ReactNode; current?: boolean; dropdownItems?: BreadcrumbDropdownItem[]; - href?: string; as?: ReactElement; } -export const BreadcrumbItem = forwardRef< - HTMLAnchorElement, - BreadcrumbItemProps ->( - ( - { - as, - children, - className, - leadingIcon, - current, - href, - dropdownItems, - ...props - }, - ref - ) => { - const renderedElement = as ?? ; - const label = ( - <> - {leadingIcon && ( - {leadingIcon} - )} - {children && {children}} - - ); +export const BreadcrumbItem = ({ + ref, + as, + children, + className, + leadingIcon, + current, + href, + dropdownItems, + ...props +}: BreadcrumbItemProps) => { + const renderedElement = as ?? ; + const label = ( + <> + {leadingIcon && ( + {leadingIcon} + )} + {children && {children}} + + ); - if (dropdownItems) { - return ( - - - {label} - - - - {dropdownItems.map((dropdownItem, dropdownIndex) => ( - - {dropdownItem.label} - - ))} - - - ); - } + if (dropdownItems) { return ( -
  • - {cloneElement( - renderedElement, - { - className: cx( - styles['breadcrumb-link'], - current && styles['breadcrumb-link-active'] - ), - href, - ...props, - ...renderedElement.props - }, - label - )} -
  • + + + {label} + + + + {dropdownItems.map((dropdownItem, dropdownIndex) => ( + + {dropdownItem.label} + + ))} + + ); } -); + return ( +
  • + {cloneElement( + renderedElement, + { + className: cx( + styles['breadcrumb-link'], + current && styles['breadcrumb-link-active'] + ), + href, + ...props, + ...renderedElement.props + }, + label + )} +
  • + ); +}; -BreadcrumbItem.displayName = 'BreadcrumbItem'; +BreadcrumbItem.displayName = 'Breadcrumb.Item'; diff --git a/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx b/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx index 52bc07c90..83e580d88 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx @@ -2,63 +2,37 @@ import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import { HTMLAttributes, forwardRef } from 'react'; +import { ComponentProps } from 'react'; import styles from './breadcrumb.module.css'; -export interface BreadcrumbEllipsisProps - extends HTMLAttributes {} +export interface BreadcrumbEllipsisProps extends ComponentProps<'span'> {} -export const BreadcrumbEllipsis = forwardRef< - HTMLSpanElement, - BreadcrumbEllipsisProps ->( - ( - { - className, - children = , - ...props - }, - ref - ) => { - return ( - - {children} - - ); - } -); +export const BreadcrumbEllipsis = ({ + className, + children = , + ...props +}: BreadcrumbEllipsisProps) => { + return ( + + {children} + + ); +}; -BreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis'; +BreadcrumbEllipsis.displayName = 'Breadcrumb.Ellipsis'; -export interface BreadcrumbSeparatorProps - extends HTMLAttributes {} +export interface BreadcrumbSeparatorProps extends ComponentProps<'span'> {} -export const BreadcrumbSeparator = forwardRef< - HTMLSpanElement, - BreadcrumbSeparatorProps ->( - ( - { - children = , - className, - ...props - }, - ref - ) => { - return ( - - {children} - - ); - } -); +export const BreadcrumbSeparator = ({ + children = , + className, + ...props +}: BreadcrumbSeparatorProps) => { + return ( + + {children} + + ); +}; -BreadcrumbSeparator.displayName = 'BreadcrumbSeparator'; +BreadcrumbSeparator.displayName = 'Breadcrumb.Separator'; diff --git a/packages/raystack/components/breadcrumb/breadcrumb-root.tsx b/packages/raystack/components/breadcrumb/breadcrumb-root.tsx index 2fdded41e..3bcf4ae9c 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-root.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-root.tsx @@ -1,7 +1,7 @@ 'use client'; -import { type VariantProps, cva } from 'class-variance-authority'; -import { HTMLAttributes, forwardRef } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { ComponentProps } from 'react'; import styles from './breadcrumb.module.css'; const breadcrumbVariants = cva(styles['breadcrumb'], { @@ -18,20 +18,19 @@ const breadcrumbVariants = cva(styles['breadcrumb'], { export interface BreadcrumbProps extends VariantProps, - HTMLAttributes {} + ComponentProps<'nav'> {} -export const BreadcrumbRoot = forwardRef( - ({ className, children, size = 'medium', ...props }, ref) => { - return ( - - ); - } -); +export const BreadcrumbRoot = ({ + className, + children, + size = 'medium', + ...props +}: BreadcrumbProps) => { + return ( + + ); +}; -BreadcrumbRoot.displayName = 'BreadcrumbRoot'; +BreadcrumbRoot.displayName = 'Breadcrumb'; diff --git a/packages/raystack/components/button/button.tsx b/packages/raystack/components/button/button.tsx index 25914fa5f..33bcbdbe7 100644 --- a/packages/raystack/components/button/button.tsx +++ b/packages/raystack/components/button/button.tsx @@ -1,6 +1,6 @@ import { Button as ButtonPrimitive } from '@base-ui/react'; import { cva, type VariantProps } from 'class-variance-authority'; -import { ElementRef, forwardRef, PropsWithChildren, ReactNode } from 'react'; +import { ReactNode } from 'react'; import { Spinner } from '../spinner'; import styles from './button.module.css'; @@ -124,7 +124,7 @@ const getLoaderOnlyClass = (size: 'small' | 'normal' | null) => ? styles['loader-only-button-small'] : styles['loader-only-button-normal']; -type ButtonProps = PropsWithChildren> & +type ButtonProps = VariantProps & ButtonPrimitive.Props & { loading?: boolean; loaderText?: ReactNode; @@ -135,69 +135,60 @@ type ButtonProps = PropsWithChildren> & style?: React.CSSProperties; }; -export const Button = forwardRef< - ElementRef, - ButtonProps ->( - ( - { - className, - variant = 'solid', - color = 'accent', - size = 'normal', - disabled, - loading, - loaderText, - leadingIcon, - trailingIcon, - maxWidth, - width, - style = {}, - children, - render, - ...props - }, - ref - ) => { - const isLoaderOnly = loading && !loaderText; - const widthStyle = { maxWidth, width }; - const buttonStyle = { ...widthStyle, ...style }; +export const Button = ({ + className, + variant = 'solid', + color = 'accent', + size = 'normal', + disabled, + loading, + loaderText, + leadingIcon, + trailingIcon, + maxWidth, + width, + style = {}, + children, + render, + ...props +}: ButtonProps) => { + const isLoaderOnly = loading && !loaderText; + const widthStyle = { maxWidth, width }; + const buttonStyle = { ...widthStyle, ...style }; - return ( - - {loading ? ( - <> - - {loaderText && ( - {loaderText} - )} - - ) : ( - <> - {leadingIcon && ( - - {leadingIcon} - - )} - {children} - {trailingIcon && ( - - {trailingIcon} - - )} - - )} - - ); - } -); + return ( + + {loading ? ( + <> + + {loaderText && ( + {loaderText} + )} + + ) : ( + <> + {leadingIcon && ( + + {leadingIcon} + + )} + {children} + {trailingIcon && ( + + {trailingIcon} + + )} + + )} + + ); +}; Button.displayName = 'Button'; diff --git a/packages/raystack/components/calendar/calendar.tsx b/packages/raystack/components/calendar/calendar.tsx index 0f3096c22..1e4c92991 100644 --- a/packages/raystack/components/calendar/calendar.tsx +++ b/packages/raystack/components/calendar/calendar.tsx @@ -210,3 +210,5 @@ export const Calendar = function ({ /> ); }; + +Calendar.displayName = 'Calendar'; diff --git a/packages/raystack/components/calendar/date-picker.tsx b/packages/raystack/components/calendar/date-picker.tsx index e06d0a317..531d19d87 100644 --- a/packages/raystack/components/calendar/date-picker.tsx +++ b/packages/raystack/components/calendar/date-picker.tsx @@ -4,7 +4,13 @@ import { CalendarIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { + isValidElement, + useCallback, + useEffect, + useRef, + useState +} from 'react'; import { PropsBase, PropsSingleRequired } from 'react-day-picker'; import { InputField } from '../input-field'; import { InputFieldProps } from '../input-field/input-field'; @@ -193,7 +199,9 @@ export function DatePicker({ return ( - {trigger} + {trigger}} + /> ); } + +DatePicker.displayName = 'DatePicker'; diff --git a/packages/raystack/components/calendar/range-picker.tsx b/packages/raystack/components/calendar/range-picker.tsx index 416cd1592..d1eec604d 100644 --- a/packages/raystack/components/calendar/range-picker.tsx +++ b/packages/raystack/components/calendar/range-picker.tsx @@ -3,7 +3,7 @@ import { CalendarIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; import dayjs from 'dayjs'; -import { useCallback, useMemo, useState } from 'react'; +import { isValidElement, useCallback, useMemo, useState } from 'react'; import { DateRange, PropsBase, PropsRangeRequired } from 'react-day-picker'; import { Flex } from '../flex'; import { InputField } from '../input-field'; @@ -167,7 +167,9 @@ export function RangePicker({ return ( - {trigger} + {trigger}} + /> ); } + +RangePicker.displayName = 'RangePicker'; From d3e0b3462b96b84901517d65f2f872439000151d Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 18 Mar 2026 01:20:00 +0530 Subject: [PATCH 02/11] fix: breaking box test --- .../components/box/__tests__/box.test.tsx | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/raystack/components/box/__tests__/box.test.tsx b/packages/raystack/components/box/__tests__/box.test.tsx index 36b61e8e1..55d71d12b 100644 --- a/packages/raystack/components/box/__tests__/box.test.tsx +++ b/packages/raystack/components/box/__tests__/box.test.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { Box } from '../box'; -import styles from '../box.module.css'; describe('Box', () => { describe('Basic Rendering', () => { @@ -34,25 +33,6 @@ describe('Box', () => { }); }); - describe('ClassName Handling', () => { - it('applies custom className', () => { - const { container } = render(Content); - const box = container.querySelector('.custom-class'); - expect(box).toBeInTheDocument(); - expect(box).toHaveClass(styles.box); - }); - - it('combines multiple classNames', () => { - const { container } = render( - Content - ); - const box = container.querySelector(`.${styles.box}`); - expect(box).toHaveClass('class1'); - expect(box).toHaveClass('class2'); - expect(box).toHaveClass(styles.box); - }); - }); - describe('Ref Forwarding', () => { it('forwards ref correctly', () => { const ref = vi.fn(); @@ -154,8 +134,8 @@ describe('Box', () => { }); it('renders with empty string children', () => { - const { container } = render({``}); - const box = container.querySelector(`.${styles.box}`); + const { container } = render({``}); + const box = container.querySelector('#test-box'); expect(box).toBeInTheDocument(); expect(box?.textContent).toBe(''); }); From 8d3bfacb147a86d8d6899d00af851a4ed8722106 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 18 Mar 2026 02:29:36 +0530 Subject: [PATCH 03/11] feat: migrate react 19 - 2 --- .../raystack/components/callout/callout.tsx | 165 +++++----- .../raystack/components/checkbox/checkbox.tsx | 18 +- .../components/code-block/code-block-code.tsx | 67 ++-- .../code-block/code-block-language-select.tsx | 17 +- .../components/code-block/code-block-misc.tsx | 143 ++++----- .../components/code-block/code-block-root.tsx | 120 ++++--- .../components/collapsible/collapsible.tsx | 50 +-- .../color-picker/color-picker-alpha.tsx | 2 + .../color-picker/color-picker-area.tsx | 8 +- .../color-picker/color-picker-hue.tsx | 2 + .../color-picker/color-picker-input.tsx | 8 +- .../color-picker/color-picker-mode.tsx | 4 +- .../color-picker/color-picker-root.tsx | 12 +- .../components/combobox/combobox-content.tsx | 78 ++--- .../components/combobox/combobox-input.tsx | 12 +- .../components/combobox/combobox-item.tsx | 104 +++--- .../components/combobox/combobox-misc.tsx | 40 +-- .../components/combobox/combobox-root.tsx | 8 +- .../raystack/components/command/command.tsx | 140 ++++---- .../components/container/container.tsx | 42 ++- .../context-menu/context-menu-content.tsx | 300 +++++++++--------- .../context-menu/context-menu-item.tsx | 70 ++-- .../context-menu/context-menu-misc.tsx | 52 ++- .../context-menu/context-menu-root.tsx | 8 +- .../context-menu/context-menu-trigger.tsx | 111 +++---- 25 files changed, 751 insertions(+), 830 deletions(-) diff --git a/packages/raystack/components/callout/callout.tsx b/packages/raystack/components/callout/callout.tsx index eee94b0b2..95e909683 100644 --- a/packages/raystack/components/callout/callout.tsx +++ b/packages/raystack/components/callout/callout.tsx @@ -1,13 +1,8 @@ 'use client'; import { InfoCircledIcon } from '@radix-ui/react-icons'; -import { type VariantProps, cva } from 'class-variance-authority'; -import { - CSSProperties, - type HTMLAttributes, - ReactNode, - forwardRef -} from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { type ComponentProps, type CSSProperties, type ReactNode } from 'react'; import styles from './callout.module.css'; @@ -35,7 +30,7 @@ const callout = cva(styles.callout, { }); export interface CalloutProps - extends HTMLAttributes, + extends ComponentProps<'div'>, VariantProps { children: ReactNode; action?: ReactNode; @@ -46,91 +41,85 @@ export interface CalloutProps icon?: ReactNode; } -export const Callout = forwardRef( - ( - { - className, - type = 'grey', - outline, - highContrast, - children, - action, - dismissible, - onDismiss, - width, - style, - icon = , - ...props - }, - ref - ) => { - const combinedStyle = { - ...style, - ...(width && { width: typeof width === 'number' ? `${width}px` : width }) - }; +export function Callout({ + className, + type = 'grey', + outline, + highContrast, + children, + action, + dismissible, + onDismiss, + width, + style, + icon = , + ...props +}: CalloutProps) { + const combinedStyle = { + ...style, + ...(width && { width: typeof width === 'number' ? `${width}px` : width }) + }; - const getRole = () => { - switch (type) { - case 'alert': - return 'alert'; - case 'success': - return 'status'; - default: - return 'status'; - } - }; + const getRole = () => { + switch (type) { + case 'alert': + return 'alert'; + case 'success': + return 'status'; + default: + return 'status'; + } + }; - return ( -
    -
    -
    - {icon && ( - - )} -
    {children}
    -
    + return ( +
    +
    +
    + {icon && ( + + )} +
    {children}
    +
    -
    - {action &&
    {action}
    } - {dismissible && ( - - )} -
    + + + + )}
    - ); - } -); +
    + ); +} Callout.displayName = 'Callout'; diff --git a/packages/raystack/components/checkbox/checkbox.tsx b/packages/raystack/components/checkbox/checkbox.tsx index 946948311..47d142583 100644 --- a/packages/raystack/components/checkbox/checkbox.tsx +++ b/packages/raystack/components/checkbox/checkbox.tsx @@ -3,7 +3,6 @@ import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'; import { CheckboxGroup as CheckboxGroupPrimitive } from '@base-ui/react/checkbox-group'; import { cx } from 'class-variance-authority'; -import { ElementRef, forwardRef } from 'react'; import styles from './checkbox.module.css'; @@ -43,26 +42,21 @@ const IndeterminateIcon = () => ( ); -const CheckboxGroup = forwardRef( - ({ className, ...props }, ref) => ( +function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) { + return ( - ) -); + ); +} CheckboxGroup.displayName = 'Checkbox.Group'; -const CheckboxItem = forwardRef< - ElementRef, - CheckboxPrimitive.Root.Props ->(({ className, ...props }, ref) => { +function CheckboxItem({ className, ...props }: CheckboxPrimitive.Root.Props) { return ( ); -}); +} CheckboxItem.displayName = 'Checkbox'; diff --git a/packages/raystack/components/code-block/code-block-code.tsx b/packages/raystack/components/code-block/code-block-code.tsx index d5588fb6f..162bbb019 100644 --- a/packages/raystack/components/code-block/code-block-code.tsx +++ b/packages/raystack/components/code-block/code-block-code.tsx @@ -2,13 +2,13 @@ import { cx } from 'class-variance-authority'; import { Highlight, Language } from 'prism-react-renderer'; -import { forwardRef, HTMLAttributes, memo } from 'react'; +import { ComponentProps, memo } from 'react'; import { useIsomorphicLayoutEffect } from '~/hooks'; import code from './code.module.css'; import styles from './code-block.module.css'; import { useCodeBlockContext } from './code-block-root'; -export interface CodeBlockCodeProps extends HTMLAttributes { +export interface CodeBlockCodeProps extends ComponentProps<'div'> { children: string; language: Language; value?: string; @@ -17,40 +17,43 @@ export interface CodeBlockCodeProps extends HTMLAttributes { const emptyTheme = { plain: {}, styles: [] }; -export const CodeBlockCode = forwardRef( - ({ children, language, className, value, ...props }, ref) => { - const { value: contextValue, setCode, setValue } = useCodeBlockContext(); - const computedValue = value ?? language; - const isContextValueDefined = !!contextValue; - const shouldRender = - !isContextValueDefined || contextValue === computedValue; - const content = children.trim(); +export const CodeBlockCode = ({ + children, + language, + className, + value, + ...props +}: CodeBlockCodeProps) => { + const { value: contextValue, setCode, setValue } = useCodeBlockContext(); + const computedValue = value ?? language; + const isContextValueDefined = !!contextValue; + const shouldRender = !isContextValueDefined || contextValue === computedValue; + const content = children.trim(); - useIsomorphicLayoutEffect(() => { - // if value is not defined, set the value - if (!isContextValueDefined) setValue(language); - // if should render, store the code - if (shouldRender) setCode(content); - }, [ - content, - setCode, - shouldRender, - setValue, - language, - isContextValueDefined - ]); + useIsomorphicLayoutEffect(() => { + // if value is not defined, set the value + if (!isContextValueDefined) setValue(language); + // if should render, store the code + if (shouldRender) setCode(content); + }, [ + content, + setCode, + shouldRender, + setValue, + language, + isContextValueDefined + ]); - if (!shouldRender) return null; + if (!shouldRender) return null; - return ( -
    - -
    - ); - } -); + return ( +
    + +
    + ); +}; -CodeBlockCode.displayName = 'CodeBlockCode'; +CodeBlockCode.displayName = 'CodeBlock.Code'; const CodeHighlight = memo( ({ content, language }: { content: string; language: Language }) => { diff --git a/packages/raystack/components/code-block/code-block-language-select.tsx b/packages/raystack/components/code-block/code-block-language-select.tsx index c3cc9acfc..8701e06ed 100644 --- a/packages/raystack/components/code-block/code-block-language-select.tsx +++ b/packages/raystack/components/code-block/code-block-language-select.tsx @@ -2,7 +2,7 @@ import { cx } from 'class-variance-authority'; import { Language } from 'prism-react-renderer'; -import { ComponentProps, ElementRef, forwardRef } from 'react'; +import { ComponentProps } from 'react'; import { Select } from '../select'; import { SingleSelectProps } from '../select/select-root'; import styles from './code-block.module.css'; @@ -17,15 +17,14 @@ export const CodeBlockLanguageSelect = (props: SingleSelectProps) => { return ); }; + +ColorPickerMode.displayName = 'ColorPicker.Mode'; diff --git a/packages/raystack/components/color-picker/color-picker-root.tsx b/packages/raystack/components/color-picker/color-picker-root.tsx index 4c7ce408d..97c59b4d2 100644 --- a/packages/raystack/components/color-picker/color-picker-root.tsx +++ b/packages/raystack/components/color-picker/color-picker-root.tsx @@ -2,14 +2,14 @@ import Color, { type ColorLike } from 'color'; import { - ComponentPropsWithoutRef, + ComponentProps, createContext, useCallback, useContext, useState } from 'react'; import { Flex } from '../flex'; -import { ColorObject, ModeType, getColorString } from './utils'; +import { ColorObject, getColorString, ModeType } from './utils'; type ColorPickerContextValue = { hue: number; @@ -34,7 +34,7 @@ export const useColorPicker = () => { }; export interface ColorPickerProps - extends Omit, 'defaultValue'> { + extends Omit, 'defaultValue'> { value?: ColorLike; defaultValue?: ColorLike; onValueChange?: (value: string, mode: string) => void; @@ -97,7 +97,7 @@ export const ColorPickerRoot = ({ ); return ( - - + ); }; + +ColorPickerRoot.displayName = 'ColorPicker'; diff --git a/packages/raystack/components/combobox/combobox-content.tsx b/packages/raystack/components/combobox/combobox-content.tsx index 02d632c7f..b13fc29f4 100644 --- a/packages/raystack/components/combobox/combobox-content.tsx +++ b/packages/raystack/components/combobox/combobox-content.tsx @@ -2,7 +2,6 @@ import { Combobox as ComboboxPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { ElementRef, forwardRef } from 'react'; import styles from './combobox.module.css'; import { useComboboxContext } from './combobox-root'; @@ -13,47 +12,40 @@ export interface ComboboxContentProps >, ComboboxPrimitive.Popup.Props {} -export const ComboboxContent = forwardRef< - ElementRef, - ComboboxContentProps ->( - ( - { - className, - children, - style, - render, - initialFocus, - finalFocus, - sideOffset = 4, - ...positionerProps - }, - ref - ) => { - const { inputContainerRef } = useComboboxContext(); - return ( - - { + const { inputContainerRef } = useComboboxContext(); + return ( + + + - - - {children} - - - - - ); - } -); + + {children} + + + + + ); +}; ComboboxContent.displayName = 'Combobox.Content'; diff --git a/packages/raystack/components/combobox/combobox-input.tsx b/packages/raystack/components/combobox/combobox-input.tsx index eb396c058..bca19aaeb 100644 --- a/packages/raystack/components/combobox/combobox-input.tsx +++ b/packages/raystack/components/combobox/combobox-input.tsx @@ -2,22 +2,18 @@ import { Combobox as ComboboxPrimitive } from '@base-ui/react'; import { ChevronDownIcon } from '@radix-ui/react-icons'; -import { ElementRef, forwardRef } from 'react'; +import { type ComponentProps } from 'react'; import { InputField } from '../input-field'; -import { InputFieldProps } from '../input-field/input-field'; import styles from './combobox.module.css'; import { useComboboxContext } from './combobox-root'; export interface ComboboxInputProps extends Omit< - InputFieldProps, + ComponentProps, 'trailingIcon' | 'suffix' | 'chips' | 'maxChipsVisible' > {} -export const ComboboxInput = forwardRef< - ElementRef, - ComboboxInputProps ->(({ ...props }, ref) => { +export const ComboboxInput = ({ ref, ...props }: ComboboxInputProps) => { const { multiple, inputContainerRef, value, onValueChange } = useComboboxContext(); return ( @@ -41,5 +37,5 @@ export const ComboboxInput = forwardRef< } /> ); -}); +}; ComboboxInput.displayName = 'Combobox.Input'; diff --git a/packages/raystack/components/combobox/combobox-item.tsx b/packages/raystack/components/combobox/combobox-item.tsx index 827d181f1..08b95518f 100644 --- a/packages/raystack/components/combobox/combobox-item.tsx +++ b/packages/raystack/components/combobox/combobox-item.tsx @@ -2,7 +2,7 @@ import { Combobox as ComboboxPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { forwardRef, ReactNode } from 'react'; +import { ReactNode } from 'react'; import { Checkbox } from '../checkbox'; import { getMatch } from '../menu/utils'; import { Text } from '../text'; @@ -13,62 +13,56 @@ export interface ComboboxItemProps extends ComboboxPrimitive.Item.Props { leadingIcon?: ReactNode; } -export const ComboboxItem = forwardRef( - ( - { - className, - children, - value: providedValue, - leadingIcon, - disabled, - render, - ...props - }, - ref - ) => { - const value = providedValue - ? providedValue - : typeof children === 'string' - ? children - : undefined; +export const ComboboxItem = ({ + className, + children, + value: providedValue, + leadingIcon, + disabled, + render, + ...props +}: ComboboxItemProps) => { + const value = providedValue + ? providedValue + : typeof children === 'string' + ? children + : undefined; - const { multiple, inputValue, hasItems } = useComboboxContext(); + const { multiple, inputValue, hasItems } = useComboboxContext(); - // When items prop is not provided on Root, use custom filtering - if (!hasItems && inputValue?.length) { - const isMatched = getMatch(value, children, inputValue); - if (!isMatched) return null; - } - - const element = - typeof children === 'string' ? ( - <> - {leadingIcon &&
    {leadingIcon}
    } - {children} - - ) : ( - children - ); + // When items prop is not provided on Root, use custom filtering + if (!hasItems && inputValue?.length) { + const isMatched = getMatch(value, children, inputValue); + if (!isMatched) return null; + } - return ( - ( -
    - {multiple && } - {element} -
    - ) - } - /> + const element = + typeof children === 'string' ? ( + <> + {leadingIcon &&
    {leadingIcon}
    } + {children} + + ) : ( + children ); - } -); + + return ( + ( +
    + {multiple && } + {element} +
    + ) + } + /> + ); +}; ComboboxItem.displayName = 'Combobox.Item'; diff --git a/packages/raystack/components/combobox/combobox-misc.tsx b/packages/raystack/components/combobox/combobox-misc.tsx index 8fa7b8716..a4a2b2d51 100644 --- a/packages/raystack/components/combobox/combobox-misc.tsx +++ b/packages/raystack/components/combobox/combobox-misc.tsx @@ -2,59 +2,53 @@ import { Combobox as ComboboxPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { ElementRef, forwardRef } from 'react'; import styles from './combobox.module.css'; import { useComboboxContext } from './combobox-root'; -export const ComboboxLabel = forwardRef< - ElementRef, - ComboboxPrimitive.GroupLabel.Props ->(({ className, ...props }, ref) => { +export const ComboboxLabel = ({ + className, + ...props +}: ComboboxPrimitive.GroupLabel.Props) => { const { inputValue, hasItems } = useComboboxContext(); if (!hasItems && inputValue?.length) return null; return ( ); -}); +}; ComboboxLabel.displayName = 'Combobox.Label'; -export const ComboboxGroup = forwardRef< - ElementRef, - ComboboxPrimitive.Group.Props ->(({ className, children, ...props }, ref) => { +export const ComboboxGroup = ({ + className, + children, + ...props +}: ComboboxPrimitive.Group.Props) => { const { inputValue, hasItems } = useComboboxContext(); if (!hasItems && inputValue?.length) return children; return ( - + {children} ); -}); +}; ComboboxGroup.displayName = 'Combobox.Group'; -export const ComboboxSeparator = forwardRef< - ElementRef, - ComboboxPrimitive.Separator.Props ->(({ className, ...props }, ref) => { +export const ComboboxSeparator = ({ + className, + ...props +}: ComboboxPrimitive.Separator.Props) => { const { inputValue, hasItems } = useComboboxContext(); if (!hasItems && inputValue?.length) return null; return ( ); -}); +}; ComboboxSeparator.displayName = 'Combobox.Separator'; diff --git a/packages/raystack/components/combobox/combobox-root.tsx b/packages/raystack/components/combobox/combobox-root.tsx index a5ec15506..d9b8e93b1 100644 --- a/packages/raystack/components/combobox/combobox-root.tsx +++ b/packages/raystack/components/combobox/combobox-root.tsx @@ -125,9 +125,7 @@ export const ComboboxRoot = ({ ); return ( - } - > + }> ({ > {children} - + ); }; + +ComboboxRoot.displayName = 'Combobox'; diff --git a/packages/raystack/components/command/command.tsx b/packages/raystack/components/command/command.tsx index fd04cc01f..203e11dd1 100644 --- a/packages/raystack/components/command/command.tsx +++ b/packages/raystack/components/command/command.tsx @@ -11,13 +11,13 @@ import { Flex } from '../flex'; import styles from './command.module.css'; const command = cva(styles.command); -const CommandRoot = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -CommandRoot.displayName = CommandPrimitive?.displayName; +function CommandRoot({ + className, + ...props +}: React.ComponentProps) { + return ; +} +CommandRoot.displayName = 'Command'; interface CommandDialogProps extends DialogPrimitive.DialogProps {} @@ -32,82 +32,80 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { }; const input = cva(styles.input); -const CommandInput = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - -)); +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} -CommandInput.displayName = CommandPrimitive.Input.displayName; +CommandInput.displayName = 'Command.Input'; const list = cva(styles.list); -const CommandList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ; +} -CommandList.displayName = CommandPrimitive.List.displayName; +CommandList.displayName = 'Command.List'; -const CommandEmpty = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->((props, ref) => ( - -)); +function CommandEmpty( + props: React.ComponentProps +) { + return ; +} -CommandEmpty.displayName = CommandPrimitive.Empty.displayName; +CommandEmpty.displayName = 'Command.Empty'; const group = cva(styles.group); -const CommandGroup = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); - -CommandGroup.displayName = CommandPrimitive.Group.displayName; +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ; +} + +CommandGroup.displayName = 'Command.Group'; const separator = cva(styles.separator); -const CommandSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -CommandSeparator.displayName = CommandPrimitive.Separator.displayName; +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} +CommandSeparator.displayName = 'Command.Separator'; const item = cva(styles.item); -const CommandItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ; +} -CommandItem.displayName = CommandPrimitive.Item.displayName; +CommandItem.displayName = 'Command.Item'; const shortcut = cva(styles.shortcut); const CommandShortcut = ({ @@ -116,7 +114,7 @@ const CommandShortcut = ({ }: React.HTMLAttributes) => { return ; }; -CommandShortcut.displayName = 'CommandShortcut'; +CommandShortcut.displayName = 'Command.Shortcut'; export const Command: any = Object.assign(CommandRoot, { Dialog: CommandDialog, diff --git a/packages/raystack/components/container/container.tsx b/packages/raystack/components/container/container.tsx index 0ff136846..70b528c34 100644 --- a/packages/raystack/components/container/container.tsx +++ b/packages/raystack/components/container/container.tsx @@ -1,49 +1,47 @@ -import { cva, VariantProps } from "class-variance-authority"; -import { HTMLAttributes, PropsWithChildren } from "react"; +import { cva, VariantProps } from 'class-variance-authority'; +import { ComponentProps, HTMLAttributes, PropsWithChildren } from 'react'; -import styles from "./container.module.css"; +import styles from './container.module.css'; const container = cva(styles.container, { variants: { size: { - small: styles["container-small"], - medium: styles["container-medium"], - large: styles["container-large"], - none: styles["container-none"], + small: styles['container-small'], + medium: styles['container-medium'], + large: styles['container-large'], + none: styles['container-none'] }, align: { - left: styles["container-align-left"], - center: styles["container-align-center"], - right: styles["container-align-right"], + left: styles['container-align-left'], + center: styles['container-align-center'], + right: styles['container-align-right'] } }, defaultVariants: { - size: "none", - align: "center", - }, + size: 'none', + align: 'center' + } }); -type ContainerProps = PropsWithChildren> & - HTMLAttributes & { - "aria-label"?: string; - "aria-labelledby"?: string; - }; +type ContainerProps = VariantProps & ComponentProps<'div'>; export function Container({ children, size, align, className, - role = "region", + role = 'region', ...props }: ContainerProps) { return ( -
    {children}
    ); -} \ No newline at end of file +} + +Container.displayName = 'Container'; diff --git a/packages/raystack/components/context-menu/context-menu-content.tsx b/packages/raystack/components/context-menu/context-menu-content.tsx index bc869a638..fd260fedd 100644 --- a/packages/raystack/components/context-menu/context-menu-content.tsx +++ b/packages/raystack/components/context-menu/context-menu-content.tsx @@ -5,7 +5,7 @@ import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { forwardRef, KeyboardEvent, useCallback, useRef } from 'react'; +import { KeyboardEvent, useCallback, useRef } from 'react'; import styles from '../menu/menu.module.css'; import { useMenuContext } from '../menu/menu-root'; import { @@ -24,79 +24,58 @@ export interface ContextMenuContentProps searchPlaceholder?: string; } -export const ContextMenuContent = forwardRef< - HTMLDivElement, - ContextMenuContentProps ->( - ( - { - className, - children, - searchPlaceholder = 'Search...', - render, - finalFocus, - style, - sideOffset = 4, - align = 'start', - onFocus, - ...positionerProps - }, - ref - ) => { - const { - autocomplete, - inputValue, - onInputValueChange, - inputRef, - isInitialRender, - parent - } = useMenuContext(); +export const ContextMenuContent = ({ + ref, + className, + children, + searchPlaceholder = 'Search...', + render, + finalFocus, + style, + sideOffset = 4, + align = 'start', + onFocus, + ...positionerProps +}: ContextMenuContentProps) => { + const { + autocomplete, + inputValue, + onInputValueChange, + inputRef, + isInitialRender, + parent + } = useMenuContext(); + + const focusInput = useCallback(() => { + if (document?.activeElement !== inputRef?.current) + inputRef?.current?.focus(); + }, [inputRef]); + const highlightedItem = useRef< + [index: number, reason: 'keyboard' | 'pointer' | 'none'] + >([-1, 'none']); + const containerRef = useRef(null); - const focusInput = useCallback(() => { - if (document?.activeElement !== inputRef?.current) - inputRef?.current?.focus(); - }, [inputRef]); - const highlightedItem = useRef< - [index: number, reason: 'keyboard' | 'pointer' | 'none'] - >([-1, 'none']); - const containerRef = useRef(null); + const highlightFirstItem = useCallback(() => { + if (!isInitialRender?.current) return; + isInitialRender.current = false; + const item = containerRef.current?.querySelector('[role="option"]'); + if (!item) return; + item.dispatchEvent(new PointerEvent('mousemove', { bubbles: true })); + }, [isInitialRender]); - const highlightFirstItem = useCallback(() => { - if (!isInitialRender?.current) return; - isInitialRender.current = false; - const item = containerRef.current?.querySelector('[role="option"]'); - if (!item) return; - item.dispatchEvent(new PointerEvent('mousemove', { bubbles: true })); - }, [isInitialRender]); + const checkAndOpenSubMenu = useCallback(() => { + if (highlightedItem.current[0] === -1) return; + const items = containerRef.current?.querySelectorAll('[role="option"]'); + const item = items?.[highlightedItem.current[0]]; + if (!item || !isElementSubMenuTrigger(item)) return; + dispatchKeyboardEvent(item, KEYCODES.ARROW_RIGHT); + }, []); - const checkAndOpenSubMenu = useCallback(() => { + const checkAndCloseSubMenu = useCallback( + (e: KeyboardEvent) => { if (highlightedItem.current[0] === -1) return; const items = containerRef.current?.querySelectorAll('[role="option"]'); const item = items?.[highlightedItem.current[0]]; - if (!item || !isElementSubMenuTrigger(item)) return; - dispatchKeyboardEvent(item, KEYCODES.ARROW_RIGHT); - }, []); - - const checkAndCloseSubMenu = useCallback( - (e: KeyboardEvent) => { - if (highlightedItem.current[0] === -1) return; - const items = containerRef.current?.querySelectorAll('[role="option"]'); - const item = items?.[highlightedItem.current[0]]; - if ( - !item || - !isElementSubMenuTrigger(item) || - !isElementSubMenuOpen(item) - ) - return; - dispatchKeyboardEvent(item, KEYCODES.ESCAPE); - e.stopPropagation(); - }, - [] - ); - - const blurStaleMenuItem = useCallback((index: number) => { - const items = containerRef.current?.querySelectorAll('[role="option"]'); - const item = items?.[index]; if ( !item || !isElementSubMenuTrigger(item) || @@ -104,99 +83,106 @@ export const ContextMenuContent = forwardRef< ) return; dispatchKeyboardEvent(item, KEYCODES.ESCAPE); - item.dispatchEvent(new PointerEvent('pointerout', { bubbles: true })); - }, []); + e.stopPropagation(); + }, + [] + ); - return ( - - { + const items = containerRef.current?.querySelectorAll('[role="option"]'); + const item = items?.[index]; + if (!item || !isElementSubMenuTrigger(item) || !isElementSubMenuOpen(item)) + return; + dispatchKeyboardEvent(item, KEYCODES.ESCAPE); + item.dispatchEvent(new PointerEvent('pointerout', { bubbles: true })); + }, []); + + return ( + + + { + focusInput(); + e.stopPropagation(); + highlightFirstItem(); + onFocus?.(e); + } + : undefined + } > - { - focusInput(); - e.stopPropagation(); - highlightFirstItem(); - onFocus?.(e); - } - : undefined - } - > - {autocomplete ? ( - onInputValueChange?.(value)} - autoHighlight={!!inputValue?.length} - mode='none' - loopFocus={false} - onItemHighlighted={(value, eventDetails) => { - if ( - highlightedItem.current[1] === 'pointer' && - eventDetails.reason === 'keyboard' - ) { - blurStaleMenuItem(highlightedItem.current[0]); - } - highlightedItem.current = [ - eventDetails.index, - eventDetails.reason - ]; + {autocomplete ? ( + onInputValueChange?.(value)} + autoHighlight={!!inputValue?.length} + mode='none' + loopFocus={false} + onItemHighlighted={(value, eventDetails) => { + if ( + highlightedItem.current[1] === 'pointer' && + eventDetails.reason === 'keyboard' + ) { + blurStaleMenuItem(highlightedItem.current[0]); + } + highlightedItem.current = [ + eventDetails.index, + eventDetails.reason + ]; + }} + > + { + focusInput(); + }} + onKeyDown={e => { + if (e.key === 'ArrowLeft') return; + if (e.key === 'Escape') return checkAndCloseSubMenu(e); + if (e.key === 'ArrowRight' || e.key === 'Enter') + checkAndOpenSubMenu(); + e.stopPropagation(); }} + tabIndex={-1} + /> + - { - focusInput(); - }} - onKeyDown={e => { - if (e.key === 'ArrowLeft') return; - if (e.key === 'Escape') return checkAndCloseSubMenu(e); - if (e.key === 'ArrowRight' || e.key === 'Enter') - checkAndOpenSubMenu(); - e.stopPropagation(); - }} - tabIndex={-1} - /> - - {children} - - - ) : ( - children - )} - - - - ); - } -); + {children} + + + ) : ( + children + )} + + + + ); +}; ContextMenuContent.displayName = 'ContextMenu.Content'; -export const ContextMenuSubContent = forwardRef< - HTMLDivElement, - ContextMenuContentProps ->(({ ...props }, ref) => ( - -)); +export const ContextMenuSubContent = ({ + ...props +}: ContextMenuContentProps) => ; ContextMenuSubContent.displayName = 'ContextMenu.SubContent'; diff --git a/packages/raystack/components/context-menu/context-menu-item.tsx b/packages/raystack/components/context-menu/context-menu-item.tsx index ad0d2ea05..40446f786 100644 --- a/packages/raystack/components/context-menu/context-menu-item.tsx +++ b/packages/raystack/components/context-menu/context-menu-item.tsx @@ -4,7 +4,6 @@ import { Autocomplete as AutocompletePrimitive, ContextMenu as ContextMenuPrimitive } from '@base-ui/react'; -import { forwardRef } from 'react'; import { Cell, CellBaseProps } from '../menu/cell'; import { useMenuContext } from '../menu/menu-root'; import { getMatch } from '../menu/utils'; @@ -15,46 +14,49 @@ export interface ContextMenuItemProps value?: string; } -export const ContextMenuItem = forwardRef( - ({ children, value, leadingIcon, trailingIcon, render, ...props }, ref) => { - const { autocomplete, inputValue, shouldFilter } = useMenuContext(); +export const ContextMenuItem = ({ + children, + value, + leadingIcon, + trailingIcon, + render, + ...props +}: ContextMenuItemProps) => { + const { autocomplete, inputValue, shouldFilter } = useMenuContext(); - const cell = render ?? ( - - ); - - // In auto mode, hide items that don't match the search value - if (shouldFilter && !getMatch(value, children, inputValue)) { - return null; - } + const cell = render ?? ( + + ); - if (autocomplete) { - return ( - } - {...props} - > - {children} - - ); - } + // In auto mode, hide items that don't match the search value + if (shouldFilter && !getMatch(value, children, inputValue)) { + return null; + } + if (autocomplete) { return ( - } {...props} - onFocus={e => { - e.stopPropagation(); - e.preventDefault(); - e.preventBaseUIHandler(); - }} > {children} - + ); } -); + + return ( + { + e.stopPropagation(); + e.preventDefault(); + e.preventBaseUIHandler(); + }} + > + {children} + + ); +}; ContextMenuItem.displayName = 'ContextMenu.Item'; diff --git a/packages/raystack/components/context-menu/context-menu-misc.tsx b/packages/raystack/components/context-menu/context-menu-misc.tsx index e5728580f..e92e80034 100644 --- a/packages/raystack/components/context-menu/context-menu-misc.tsx +++ b/packages/raystack/components/context-menu/context-menu-misc.tsx @@ -2,15 +2,16 @@ import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { Fragment, forwardRef, HTMLAttributes, ReactNode } from 'react'; +import { ComponentProps, Fragment, HTMLAttributes, ReactNode } from 'react'; import styles from '../menu/menu.module.css'; import { useMenuContext } from '../menu/menu-root'; export type ContextMenuGroupProps = ContextMenuPrimitive.Group.Props; -export const ContextMenuGroup = forwardRef< - HTMLDivElement, - ContextMenuGroupProps ->(({ className, children, ...props }, ref) => { +export const ContextMenuGroup = ({ + className, + children, + ...props +}: ContextMenuGroupProps) => { const { shouldFilter } = useMenuContext(); if (shouldFilter) { @@ -18,18 +19,18 @@ export const ContextMenuGroup = forwardRef< } return ( - + {children} ); -}); +}; ContextMenuGroup.displayName = 'ContextMenu.Group'; export type ContextMenuLabelProps = ContextMenuPrimitive.GroupLabel.Props; -export const ContextMenuLabel = forwardRef< - HTMLDivElement, - ContextMenuLabelProps ->(({ className, ...props }, ref) => { +export const ContextMenuLabel = ({ + className, + ...props +}: ContextMenuLabelProps) => { const { shouldFilter } = useMenuContext(); if (shouldFilter) { @@ -38,18 +39,17 @@ export const ContextMenuLabel = forwardRef< return ( ); -}); +}; ContextMenuLabel.displayName = 'ContextMenu.Label'; -export const ContextMenuSeparator = forwardRef< - HTMLDivElement, - HTMLAttributes ->(({ className, ...props }, ref) => { +export const ContextMenuSeparator = ({ + className, + ...props +}: HTMLAttributes) => { const { shouldFilter } = useMenuContext(); if (shouldFilter) { @@ -58,23 +58,21 @@ export const ContextMenuSeparator = forwardRef< return (
    ); -}); +}; ContextMenuSeparator.displayName = 'ContextMenu.Separator'; -export const ContextMenuEmptyState = forwardRef< - HTMLDivElement, - HTMLAttributes & { - children: ReactNode; - } ->(({ className, children, ...props }, ref) => ( -
    +export const ContextMenuEmptyState = ({ + className, + children, + ...props +}: ComponentProps<'div'>) => ( +
    {children}
    -)); +); ContextMenuEmptyState.displayName = 'ContextMenu.EmptyState'; diff --git a/packages/raystack/components/context-menu/context-menu-root.tsx b/packages/raystack/components/context-menu/context-menu-root.tsx index 331b9ca55..f9b00cb65 100644 --- a/packages/raystack/components/context-menu/context-menu-root.tsx +++ b/packages/raystack/components/context-menu/context-menu-root.tsx @@ -73,7 +73,7 @@ export const ContextMenuRoot = ({ ); return ( - - + ); }; ContextMenuRoot.displayName = 'ContextMenu'; @@ -164,7 +164,7 @@ export const ContextMenuSubMenu = ({ ); return ( - - + ); }; ContextMenuSubMenu.displayName = 'ContextMenu.SubMenu'; diff --git a/packages/raystack/components/context-menu/context-menu-trigger.tsx b/packages/raystack/components/context-menu/context-menu-trigger.tsx index a09345bfd..34752c872 100644 --- a/packages/raystack/components/context-menu/context-menu-trigger.tsx +++ b/packages/raystack/components/context-menu/context-menu-trigger.tsx @@ -4,7 +4,6 @@ import { Autocomplete as AutocompletePrimitive, ContextMenu as ContextMenuPrimitive } from '@base-ui/react'; -import { forwardRef } from 'react'; import { TriangleRightIcon } from '~/icons'; import { Cell, CellBaseProps } from '../menu/cell'; import { useMenuContext } from '../menu/menu-root'; @@ -13,16 +12,16 @@ import { getMatch } from '../menu/utils'; export interface ContextMenuTriggerProps extends ContextMenuPrimitive.Trigger.Props {} -export const ContextMenuTrigger = forwardRef< - HTMLDivElement, - ContextMenuTriggerProps ->(({ children, ...props }, ref) => { +export const ContextMenuTrigger = ({ + children, + ...props +}: ContextMenuTriggerProps) => { return ( - + {children} ); -}); +}; ContextMenuTrigger.displayName = 'ContextMenu.Trigger'; export interface ContextMenuSubTriggerProps @@ -31,61 +30,49 @@ export interface ContextMenuSubTriggerProps value?: string; } -export const ContextMenuSubTrigger = forwardRef< - HTMLDivElement, - ContextMenuSubTriggerProps ->( - ( - { - children, - value, - trailingIcon = , - leadingIcon, - ...props - }, - ref - ) => { - const { parent, inputRef } = useMenuContext(); +export const ContextMenuSubTrigger = ({ + children, + value, + trailingIcon = , + leadingIcon, + ...props +}: ContextMenuSubTriggerProps) => { + const { parent, inputRef } = useMenuContext(); - if ( - parent?.shouldFilter && - !getMatch(value, children, parent?.inputValue) - ) { - return null; - } - - const cell = ; - return ( - { - if (document?.activeElement !== parent?.inputRef?.current) - parent?.inputRef?.current?.focus(); - props?.onPointerEnter?.(e); - }} - onKeyDown={e => { - requestAnimationFrame(() => { - inputRef?.current?.focus(); - }); - props?.onKeyDown?.(e); - }} - /> - ) : ( - cell - ) - } - {...props} - role={parent?.autocomplete ? 'option' : 'menuitem'} - data-slot='menu-subtrigger' - > - {children} - - ); + if (parent?.shouldFilter && !getMatch(value, children, parent?.inputValue)) { + return null; } -); + + const cell = ; + return ( + { + if (document?.activeElement !== parent?.inputRef?.current) + parent?.inputRef?.current?.focus(); + props?.onPointerEnter?.(e); + }} + onKeyDown={e => { + requestAnimationFrame(() => { + inputRef?.current?.focus(); + }); + props?.onKeyDown?.(e); + }} + /> + ) : ( + cell + ) + } + {...props} + role={parent?.autocomplete ? 'option' : 'menuitem'} + data-slot='menu-subtrigger' + > + {children} + + ); +}; ContextMenuSubTrigger.displayName = 'ContextMenu.SubTrigger'; From f6428f153235626f370d1de4f39c4af2ca1d4f08 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 18 Mar 2026 03:09:11 +0530 Subject: [PATCH 04/11] feat: migrate react to React 19 - 3 --- .../components/copy-button/copy-button.tsx | 21 ++- .../data-table/components/content.tsx | 2 + .../components/display-settings.tsx | 6 +- .../data-table/components/filters.tsx | 2 + .../data-table/components/search.tsx | 94 +++++++------- .../data-table/components/toolbar.tsx | 2 + .../components/virtualized-content.tsx | 2 + .../components/data-table/data-table.tsx | 8 +- .../components/dialog/dialog-content.tsx | 80 +++++------- .../components/dialog/dialog-misc.tsx | 115 ++++++++--------- .../components/drawer/drawer-content.tsx | 87 ++++++------- .../components/drawer/drawer-misc.tsx | 64 ++++----- .../components/drawer/drawer-root.tsx | 9 +- .../components/empty-state/empty-state.tsx | 32 +++-- packages/raystack/components/flex/flex.tsx | 57 ++++---- .../raystack/components/grid/grid-item.tsx | 77 ++++++----- packages/raystack/components/grid/grid.tsx | 122 +++++++++--------- .../raystack/components/headline/headline.tsx | 45 +++---- .../components/icon-button/icon-button.tsx | 33 ++--- 19 files changed, 408 insertions(+), 450 deletions(-) diff --git a/packages/raystack/components/copy-button/copy-button.tsx b/packages/raystack/components/copy-button/copy-button.tsx index d080405a9..94ddfbf51 100644 --- a/packages/raystack/components/copy-button/copy-button.tsx +++ b/packages/raystack/components/copy-button/copy-button.tsx @@ -1,7 +1,7 @@ 'use client'; import { CopyIcon } from '@radix-ui/react-icons'; -import { ElementRef, forwardRef, useState } from 'react'; +import { useState } from 'react'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; import { CheckCircleFilledIcon } from '~/icons'; import { IconButton, IconButtonProps } from '../icon-button/icon-button'; @@ -12,10 +12,12 @@ export interface CopyButtonProps extends IconButtonProps { resetIcon?: boolean; } -export const CopyButton = forwardRef< - ElementRef, - CopyButtonProps ->(({ text, resetTimeout = 1000, resetIcon = true, ...props }, ref) => { +export function CopyButton({ + text, + resetTimeout = 1000, + resetIcon = true, + ...props +}: CopyButtonProps) { const { copy } = useCopyToClipboard(); const [isCopied, setIsCopied] = useState(false); @@ -32,12 +34,7 @@ export const CopyButton = forwardRef< } return ( - + {isCopied ? ( ) : ( @@ -45,6 +42,6 @@ export const CopyButton = forwardRef< )} ); -}); +} CopyButton.displayName = 'CopyButton'; diff --git a/packages/raystack/components/data-table/components/content.tsx b/packages/raystack/components/data-table/components/content.tsx index 9f035a7df..08020f679 100644 --- a/packages/raystack/components/data-table/components/content.tsx +++ b/packages/raystack/components/data-table/components/content.tsx @@ -286,3 +286,5 @@ export function Content({
    ); } + +Content.displayName = 'DataTable.Content'; diff --git a/packages/raystack/components/data-table/components/display-settings.tsx b/packages/raystack/components/data-table/components/display-settings.tsx index a25f1bcc7..40c19827d 100644 --- a/packages/raystack/components/data-table/components/display-settings.tsx +++ b/packages/raystack/components/data-table/components/display-settings.tsx @@ -9,8 +9,8 @@ import { Popover } from '../../popover'; import styles from '../data-table.module.css'; import { DataTableColumn, - SortOrdersValues, - defaultGroupOption + defaultGroupOption, + SortOrdersValues } from '../data-table.types'; import { useDataTable } from '../hooks/useDataTable'; import { DisplayProperties } from './display-properties'; @@ -124,3 +124,5 @@ export function DisplaySettings({ ); } + +DisplaySettings.displayName = 'DataTable.DisplayControls'; diff --git a/packages/raystack/components/data-table/components/filters.tsx b/packages/raystack/components/data-table/components/filters.tsx index d5c685dfa..17957f49d 100644 --- a/packages/raystack/components/data-table/components/filters.tsx +++ b/packages/raystack/components/data-table/components/filters.tsx @@ -166,3 +166,5 @@ export function Filters({ ); } + +Filters.displayName = 'DataTable.Filters'; diff --git a/packages/raystack/components/data-table/components/search.tsx b/packages/raystack/components/data-table/components/search.tsx index 371bde378..a7b909d5d 100644 --- a/packages/raystack/components/data-table/components/search.tsx +++ b/packages/raystack/components/data-table/components/search.tsx @@ -1,11 +1,10 @@ 'use client'; -import { forwardRef } from 'react'; +import { ChangeEvent, type ComponentProps } from 'react'; import { Search } from '../../search'; -import { SearchProps } from '../../search/search'; import { useDataTable } from '../hooks/useDataTable'; -export interface DataTableSearchProps extends SearchProps { +export interface DataTableSearchProps extends ComponentProps { /** * Automatically disable search in zero state (when no data and no filters/search applied). * @defaultValue true @@ -13,52 +12,53 @@ export interface DataTableSearchProps extends SearchProps { autoDisableInZeroState?: boolean; } -export const TableSearch = forwardRef( - ({ autoDisableInZeroState = true, disabled, ...props }, ref) => { - const { - updateTableQuery, - tableQuery, - shouldShowFilters = false - } = useDataTable(); +export function TableSearch({ + autoDisableInZeroState = true, + disabled, + ...props +}: DataTableSearchProps) { + const { + updateTableQuery, + tableQuery, + shouldShowFilters = false + } = useDataTable(); - const handleSearch = (e: React.ChangeEvent) => { - const value = e.target.value; - updateTableQuery(query => { - return { - ...query, - search: value - }; - }); - }; + const handleSearch = (e: ChangeEvent) => { + const value = e.target.value; + updateTableQuery(query => { + return { + ...query, + search: value + }; + }); + }; - const handleClear = () => { - updateTableQuery(query => { - return { - ...query, - search: '' - }; - }); - }; + const handleClear = () => { + updateTableQuery(query => { + return { + ...query, + search: '' + }; + }); + }; - // Auto-disable in zero state if enabled, but allow manual override - // Once search is applied, keep it enabled (even if shouldShowFilters is false) - const hasSearch = Boolean( - tableQuery?.search && tableQuery.search.trim() !== '' - ); - const isDisabled = - disabled ?? (autoDisableInZeroState && !shouldShowFilters && !hasSearch); + // Auto-disable in zero state if enabled, but allow manual override + // Once search is applied, keep it enabled (even if shouldShowFilters is false) + const hasSearch = Boolean( + tableQuery?.search && tableQuery.search.trim() !== '' + ); + const isDisabled = + disabled ?? (autoDisableInZeroState && !shouldShowFilters && !hasSearch); - return ( - - ); - } -); + return ( + + ); +} -TableSearch.displayName = 'TableSearch'; +TableSearch.displayName = 'DataTable.Search'; diff --git a/packages/raystack/components/data-table/components/toolbar.tsx b/packages/raystack/components/data-table/components/toolbar.tsx index 75d13011a..9669219f0 100644 --- a/packages/raystack/components/data-table/components/toolbar.tsx +++ b/packages/raystack/components/data-table/components/toolbar.tsx @@ -25,3 +25,5 @@ export function Toolbar({ className }: { className?: string }) { ); } + +Toolbar.displayName = 'DataTable.Toolbar'; diff --git a/packages/raystack/components/data-table/components/virtualized-content.tsx b/packages/raystack/components/data-table/components/virtualized-content.tsx index 8321c7196..d6f3b5a9b 100644 --- a/packages/raystack/components/data-table/components/virtualized-content.tsx +++ b/packages/raystack/components/data-table/components/virtualized-content.tsx @@ -377,3 +377,5 @@ export function VirtualizedContent({
    ); } + +VirtualizedContent.displayName = 'DataTable.VirtualizedContent'; diff --git a/packages/raystack/components/data-table/data-table.tsx b/packages/raystack/components/data-table/data-table.tsx index 9588d59a2..2e7c5516b 100644 --- a/packages/raystack/components/data-table/data-table.tsx +++ b/packages/raystack/components/data-table/data-table.tsx @@ -215,13 +215,11 @@ function DataTableRoot({ stickyGroupHeader ]); - return ( - - {children} - - ); + return {children}; } +DataTableRoot.displayName = 'DataTable'; + export const DataTable = Object.assign(DataTableRoot, { Content: Content, VirtualizedContent: VirtualizedContent, diff --git a/packages/raystack/components/dialog/dialog-content.tsx b/packages/raystack/components/dialog/dialog-content.tsx index 98c75eae2..4d9ea7c2a 100644 --- a/packages/raystack/components/dialog/dialog-content.tsx +++ b/packages/raystack/components/dialog/dialog-content.tsx @@ -2,7 +2,6 @@ import { Dialog as DialogPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { type ElementRef, forwardRef } from 'react'; import styles from './dialog.module.css'; import { CloseButton } from './dialog-misc'; @@ -17,51 +16,42 @@ export interface DialogContentProps extends DialogPrimitive.Popup.Props { showNestedAnimation?: boolean; } -export const DialogContent = forwardRef< - ElementRef, - DialogContentProps ->( - ( - { - className, - children, - showCloseButton = true, - overlay, - width, - style, - showNestedAnimation = true, - ...props - }, - ref - ) => { - return ( - - + + + - - - {children} - {showCloseButton && } - - - - ); - } -); + style={{ width, ...style }} + {...props} + > + {children} + {showCloseButton && } + + + + ); +} DialogContent.displayName = 'Dialog.Content'; diff --git a/packages/raystack/components/dialog/dialog-misc.tsx b/packages/raystack/components/dialog/dialog-misc.tsx index fd07ce9b7..218948816 100644 --- a/packages/raystack/components/dialog/dialog-misc.tsx +++ b/packages/raystack/components/dialog/dialog-misc.tsx @@ -3,62 +3,64 @@ import { Dialog as DialogPrimitive } from '@base-ui/react'; import { Cross1Icon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import { ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react'; +import { type ComponentProps } from 'react'; import { Flex } from '../flex'; import styles from './dialog.module.css'; -export const DialogHeader = forwardRef< - ElementRef, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +export function DialogHeader({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} DialogHeader.displayName = 'Dialog.Header'; -export const DialogFooter = forwardRef< - ElementRef, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +export function DialogFooter({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} DialogFooter.displayName = 'Dialog.Footer'; -export const DialogBody = forwardRef< - ElementRef, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); +export function DialogBody({ + className, + ...props +}: ComponentProps) { + return ( + + ); +} DialogBody.displayName = 'Dialog.Body'; -export const CloseButton = forwardRef< - ElementRef, - DialogPrimitive.Close.Props ->(({ className, ...props }, ref) => { +export function CloseButton({ + className, + ...props +}: DialogPrimitive.Close.Props) { return ( ); -}); +} CloseButton.displayName = 'Dialog.CloseButton'; -export const DialogTitle = forwardRef< - ElementRef, - DialogPrimitive.Title.Props ->(({ className, ...props }, ref) => { +export function DialogTitle({ + className, + ...props +}: DialogPrimitive.Title.Props) { return ( - + ); -}); +} DialogTitle.displayName = 'Dialog.Title'; -export const DialogDescription = forwardRef< - ElementRef, - DialogPrimitive.Description.Props ->(({ className, ...props }, ref) => { +export function DialogDescription({ + className, + ...props +}: DialogPrimitive.Description.Props) { return ( ); -}); +} DialogDescription.displayName = 'Dialog.Description'; diff --git a/packages/raystack/components/drawer/drawer-content.tsx b/packages/raystack/components/drawer/drawer-content.tsx index c316097ae..4d9676def 100644 --- a/packages/raystack/components/drawer/drawer-content.tsx +++ b/packages/raystack/components/drawer/drawer-content.tsx @@ -3,11 +3,9 @@ import { DrawerPreview as DrawerPrimitive } from '@base-ui/react/drawer'; import { Cross1Icon } from '@radix-ui/react-icons'; import { cva, cx, type VariantProps } from 'class-variance-authority'; -import { type ElementRef, forwardRef } from 'react'; +import { ReactNode } from 'react'; import styles from './drawer.module.css'; -type Side = 'top' | 'right' | 'bottom' | 'left'; - const drawerPopup = cva(styles.drawerPopup, { variants: { side: { @@ -27,52 +25,43 @@ export interface DrawerContentProps VariantProps { showCloseButton?: boolean; overlayProps?: DrawerPrimitive.Backdrop.Props; - children?: React.ReactNode; + children?: ReactNode; } -export const DrawerContent = forwardRef< - ElementRef, - DrawerContentProps ->( - ( - { - className, - children, - side = 'right', - showCloseButton = true, - overlayProps, - ...props - }, - ref - ) => { - return ( - - - - - - {children} - {showCloseButton && ( - - - )} - - - - - ); - } -); +export function DrawerContent({ + className, + children, + side = 'right', + showCloseButton = true, + overlayProps, + ...props +}: DrawerContentProps) { + return ( + + + + + + {children} + {showCloseButton && ( + + + )} + + + + + ); +} DrawerContent.displayName = 'Drawer.Content'; diff --git a/packages/raystack/components/drawer/drawer-misc.tsx b/packages/raystack/components/drawer/drawer-misc.tsx index 785c4c5ec..3cd74a509 100644 --- a/packages/raystack/components/drawer/drawer-misc.tsx +++ b/packages/raystack/components/drawer/drawer-misc.tsx @@ -2,12 +2,7 @@ import { DrawerPreview as DrawerPrimitive } from '@base-ui/react/drawer'; import { cx } from 'class-variance-authority'; -import { - type ElementRef, - forwardRef, - type HTMLAttributes, - type ReactNode -} from 'react'; +import { ComponentProps, type ReactNode } from 'react'; import styles from './drawer.module.css'; export const DrawerHeader = ({ @@ -19,42 +14,35 @@ export const DrawerHeader = ({ }) =>
    {children}
    ; DrawerHeader.displayName = 'Drawer.Header'; -export const DrawerTitle = forwardRef< - ElementRef, - DrawerPrimitive.Title.Props ->(({ className, ...props }, ref) => ( - -)); +export function DrawerTitle({ + className, + ...props +}: DrawerPrimitive.Title.Props) { + return ( + + ); +} DrawerTitle.displayName = 'Drawer.Title'; -export const DrawerDescription = forwardRef< - ElementRef, - DrawerPrimitive.Description.Props ->(({ className, ...props }, ref) => ( - -)); +export function DrawerDescription({ + className, + ...props +}: DrawerPrimitive.Description.Props) { + return ( + + ); +} DrawerDescription.displayName = 'Drawer.Description'; -export const DrawerBody = forwardRef< - HTMLDivElement, - HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)); +export function DrawerBody({ className, ...props }: ComponentProps<'div'>) { + return
    ; +} DrawerBody.displayName = 'Drawer.Body'; -export const DrawerFooter = forwardRef< - HTMLDivElement, - HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)); +export function DrawerFooter({ className, ...props }: ComponentProps<'div'>) { + return
    ; +} DrawerFooter.displayName = 'Drawer.Footer'; diff --git a/packages/raystack/components/drawer/drawer-root.tsx b/packages/raystack/components/drawer/drawer-root.tsx index c841fa8ca..e30bb4f07 100644 --- a/packages/raystack/components/drawer/drawer-root.tsx +++ b/packages/raystack/components/drawer/drawer-root.tsx @@ -4,10 +4,13 @@ import { DrawerPreview as DrawerPrimitive } from '@base-ui/react/drawer'; type Side = 'top' | 'right' | 'bottom' | 'left'; -const sideToSwipeDirection: Record = { - top: 'top', +const sideToSwipeDirection: Record< + Side, + DrawerPrimitive.Root.Props['swipeDirection'] +> = { + top: 'up', right: 'right', - bottom: 'bottom', + bottom: 'down', left: 'left' }; diff --git a/packages/raystack/components/empty-state/empty-state.tsx b/packages/raystack/components/empty-state/empty-state.tsx index 46ef7e3d7..63430485b 100644 --- a/packages/raystack/components/empty-state/empty-state.tsx +++ b/packages/raystack/components/empty-state/empty-state.tsx @@ -1,4 +1,5 @@ import { cx } from 'class-variance-authority'; +import { ComponentProps, ReactNode } from 'react'; import { Flex } from '../flex'; import { Text } from '../text'; import styles from './empty-state.module.css'; @@ -10,28 +11,34 @@ type classNameKeys = | 'heading' | 'subHeading'; -interface EmptyStateProps { - icon: React.ReactNode; - heading?: React.ReactNode; - subHeading?: React.ReactNode; - primaryAction?: React.ReactNode; - secondaryAction?: React.ReactNode; +interface EmptyStateProps extends ComponentProps { + icon: ReactNode; + heading?: ReactNode; + subHeading?: ReactNode; + primaryAction?: ReactNode; + secondaryAction?: ReactNode; classNames?: Partial>; variant?: 'empty1' | 'empty2'; } -export const EmptyState = ({ +export function EmptyState({ icon, heading, subHeading, primaryAction, secondaryAction, classNames, - variant = 'empty1' -}: EmptyStateProps) => { + variant = 'empty1', + ...props +}: EmptyStateProps) { if (variant === 'empty2') { return ( - +
    {icon}
    @@ -113,4 +121,6 @@ export const EmptyState = ({ {secondaryAction} ); -}; +} + +EmptyState.displayName = 'EmptyState'; diff --git a/packages/raystack/components/flex/flex.tsx b/packages/raystack/components/flex/flex.tsx index e2a3acd8d..e7aa94c7c 100644 --- a/packages/raystack/components/flex/flex.tsx +++ b/packages/raystack/components/flex/flex.tsx @@ -1,6 +1,5 @@ import { mergeProps, useRender } from '@base-ui/react'; import { cva, VariantProps } from 'class-variance-authority'; -import { forwardRef } from 'react'; import styles from './flex.module.css'; const flex = cva(styles.flex, { @@ -59,40 +58,38 @@ const flex = cva(styles.flex, { type BoxProps = VariantProps & useRender.ComponentProps<'div'>; -export const Flex = forwardRef( - ( - { +export function Flex({ + direction, + align, + justify, + wrap, + gap, + className, + width, + render, + ref, + ...props +}: BoxProps) { + const flexProps = { + className: flex({ direction, align, justify, wrap, gap, className, - width, - render, - ...props - }, - ref - ) => { - const flexProps = { - className: flex({ - direction, - align, - justify, - wrap, - gap, - className, - width - }) - }; + width + }) + }; - const element = useRender({ - defaultTagName: 'div', - ref, - render, - props: mergeProps<'div'>(flexProps, props) - }); + const element = useRender({ + defaultTagName: 'div', + ref, + render, + props: mergeProps<'div'>(flexProps, props) + }); - return element; - } -); + return element; +} + +Flex.displayName = 'Flex'; diff --git a/packages/raystack/components/grid/grid-item.tsx b/packages/raystack/components/grid/grid-item.tsx index 18cc77d12..1078f61e6 100644 --- a/packages/raystack/components/grid/grid-item.tsx +++ b/packages/raystack/components/grid/grid-item.tsx @@ -1,5 +1,4 @@ import { mergeProps, useRender } from '@base-ui/react'; -import { forwardRef } from 'react'; import { AlignType } from './types'; type GridItemProps = useRender.ComponentProps<'div'> & { @@ -14,46 +13,42 @@ type GridItemProps = useRender.ComponentProps<'div'> & { alignSelf?: AlignType; }; -export const GridItem = forwardRef( - ( - { - area, - colStart, - colEnd, - rowStart, - rowEnd, - colSpan, - rowSpan, - justifySelf, - alignSelf, - style, - render, - ...props - }, - ref - ) => { - const gridItemStyle = { - gridArea: area, - gridColumnStart: colStart, - gridColumnEnd: colEnd, - gridRowStart: rowStart, - gridRowEnd: rowEnd, - gridColumn: colSpan ? `span ${colSpan}` : undefined, - gridRow: rowSpan ? `span ${rowSpan}` : undefined, - justifySelf, - alignSelf, - ...style - }; +export function GridItem({ + area, + colStart, + colEnd, + rowStart, + rowEnd, + colSpan, + rowSpan, + justifySelf, + alignSelf, + style, + render, + ref, + ...props +}: GridItemProps) { + const gridItemStyle = { + gridArea: area, + gridColumnStart: colStart, + gridColumnEnd: colEnd, + gridRowStart: rowStart, + gridRowEnd: rowEnd, + gridColumn: colSpan ? `span ${colSpan}` : undefined, + gridRow: rowSpan ? `span ${rowSpan}` : undefined, + justifySelf, + alignSelf, + ...style + }; - const element = useRender({ - defaultTagName: 'div', - ref, - render, - props: mergeProps<'div'>({ style: gridItemStyle }, props) - }); + const element = useRender({ + defaultTagName: 'div', + ref, + render, + props: mergeProps<'div'>({ style: gridItemStyle }, props) + }); - return element; - } -); + return element; +} -GridItem.displayName = 'GridItem'; +GridItem.displayName = 'Grid.Item'; diff --git a/packages/raystack/components/grid/grid.tsx b/packages/raystack/components/grid/grid.tsx index 77cc55b88..74f80696e 100644 --- a/packages/raystack/components/grid/grid.tsx +++ b/packages/raystack/components/grid/grid.tsx @@ -1,5 +1,5 @@ import { mergeProps, useRender } from '@base-ui/react'; -import { forwardRef, useMemo } from 'react'; +import { useMemo } from 'react'; import { AlignExtendedType, AlignType } from './types'; const GAPS = { @@ -56,71 +56,67 @@ type GridProps = useRender.ComponentProps<'div'> & { inline?: boolean; }; -export const Grid = forwardRef( - ( - { - templateAreas, - autoFlow, - autoColumns, - autoRows, - columns, - rows, - gap, - columnGap, - rowGap, - justifyItems, - alignItems, - justifyContent, - alignContent, - inline = false, - style = {}, - render, - ...props - }, - ref - ) => { - const gridTemplateColumns = - typeof columns === 'number' ? `repeat(${columns}, 1fr)` : columns; - const gridTemplateRows = - typeof rows === 'number' ? `repeat(${rows}, 1fr)` : rows; +export function Grid({ + templateAreas, + autoFlow, + autoColumns, + autoRows, + columns, + rows, + gap, + columnGap, + rowGap, + justifyItems, + alignItems, + justifyContent, + alignContent, + inline = false, + style = {}, + render, + ref, + ...props +}: GridProps) { + const gridTemplateColumns = + typeof columns === 'number' ? `repeat(${columns}, 1fr)` : columns; + const gridTemplateRows = + typeof rows === 'number' ? `repeat(${rows}, 1fr)` : rows; - const gridTemplateAreas = useMemo(() => { - if (Array.isArray(templateAreas)) { - return templateAreas - .map(area => `"${area}"`) - .join(' ') - .trim(); - } - return templateAreas; - }, [templateAreas]); + const gridTemplateAreas = useMemo(() => { + if (Array.isArray(templateAreas)) { + return templateAreas + .map(area => `"${area}"`) + .join(' ') + .trim(); + } + return templateAreas; + }, [templateAreas]); - const gridStyle = { - display: inline ? 'inline-grid' : 'grid', - gridTemplateAreas, - gridAutoFlow: autoFlow, - gridAutoColumns: autoColumns, - gridAutoRows: autoRows, - gridTemplateColumns, - gridTemplateRows, - gap: gap && GAPS[gap], - columnGap: columnGap && GAPS[columnGap], - rowGap: rowGap && GAPS[rowGap], - justifyItems, - alignItems, - justifyContent, - alignContent, - ...style - }; + const gridStyle = { + display: inline ? 'inline-grid' : 'grid', + gridTemplateAreas, + gridAutoFlow: autoFlow, + gridAutoColumns: autoColumns, + gridAutoRows: autoRows, + gridTemplateColumns, + gridTemplateRows, + gap: gap && GAPS[gap], + columnGap: columnGap && GAPS[columnGap], + rowGap: rowGap && GAPS[rowGap], + justifyItems, + alignItems, + justifyContent, + alignContent, + ...style + }; - const element = useRender({ - defaultTagName: 'div', - ref, - render, - props: mergeProps<'div'>({ style: gridStyle }, props) - }); + const element = useRender({ + defaultTagName: 'div', + ref, + render, + props: mergeProps<'div'>({ style: gridStyle }, props) + }); - return element; - } -); + return element; +} Grid.displayName = 'Grid'; diff --git a/packages/raystack/components/headline/headline.tsx b/packages/raystack/components/headline/headline.tsx index 2e2b2ea47..dd09a72b1 100644 --- a/packages/raystack/components/headline/headline.tsx +++ b/packages/raystack/components/headline/headline.tsx @@ -1,6 +1,5 @@ -import { type VariantProps, cva } from 'class-variance-authority'; -import { HTMLAttributes, forwardRef } from 'react'; - +import { cva, type VariantProps } from 'class-variance-authority'; +import { ComponentProps } from 'react'; import styles from './headline.module.css'; const headline = cva(styles.headline, { @@ -43,31 +42,25 @@ export type HeadlineBaseProps = VariantProps & { }; type HeadlineProps = HeadlineBaseProps & - HTMLAttributes & { + ComponentProps<'h1'> & { as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; }; -export const Headline = forwardRef( - ( - { - className, - size, - weight, - align, - truncate, - as: Component = 'h2', - ...props - }, - ref - ) => { - return ( - - ); - } -); +export function Headline({ + className, + size, + weight, + align, + truncate, + as: Component = 'h2', + ...props +}: HeadlineProps) { + return ( + + ); +} Headline.displayName = 'Headline'; diff --git a/packages/raystack/components/icon-button/icon-button.tsx b/packages/raystack/components/icon-button/icon-button.tsx index ecc36f2fd..c524ccde7 100644 --- a/packages/raystack/components/icon-button/icon-button.tsx +++ b/packages/raystack/components/icon-button/icon-button.tsx @@ -1,7 +1,6 @@ import { cva, VariantProps } from 'class-variance-authority'; -import { ElementRef, forwardRef } from 'react'; +import { ComponentProps } from 'react'; import { Flex } from '../flex'; - import styles from './icon-button.module.css'; const iconButton = cva(styles.iconButton, { @@ -19,27 +18,23 @@ const iconButton = cva(styles.iconButton, { }); export interface IconButtonProps - extends React.ButtonHTMLAttributes, + extends ComponentProps<'button'>, VariantProps { size?: 1 | 2 | 3 | 4; 'aria-label'?: string; } -export const IconButton = forwardRef, IconButtonProps>( - ( - { - className, - size, - disabled, - children, - 'aria-label': ariaLabel, - style, - ...props - }, - ref - ) => ( +export function IconButton({ + className, + size, + disabled, + children, + 'aria-label': ariaLabel, + style, + ...props +}: IconButtonProps) { + return ( - ) -); + ); +} IconButton.displayName = 'IconButton'; From 9c2c71b6050a6a4afe34f71ce85a05c4dd228bfe Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 18 Mar 2026 03:30:10 +0530 Subject: [PATCH 05/11] feat: migrate react 19 - 4 --- packages/raystack/components/image/image.tsx | 14 +- .../components/indicator/indicator.tsx | 50 ++- .../components/input-field/input-field.tsx | 227 +++++++------ packages/raystack/components/label/label.tsx | 51 +-- packages/raystack/components/link/link.tsx | 93 +++--- packages/raystack/components/list/list.tsx | 22 +- packages/raystack/components/menu/cell.tsx | 24 +- .../raystack/components/menu/menu-content.tsx | 298 +++++++++--------- .../raystack/components/menu/menu-item.tsx | 70 ++-- .../raystack/components/menu/menu-misc.tsx | 70 ++-- .../raystack/components/menu/menu-root.tsx | 10 +- .../raystack/components/menu/menu-trigger.tsx | 153 +++++---- .../raystack/components/menubar/menubar.tsx | 21 +- .../raystack/components/meter/meter-misc.tsx | 41 ++- .../raystack/components/meter/meter-root.tsx | 75 ++--- .../raystack/components/meter/meter-track.tsx | 22 +- .../components/navbar/navbar-root.tsx | 40 +-- .../components/navbar/navbar-sections.tsx | 35 +- 18 files changed, 631 insertions(+), 685 deletions(-) diff --git a/packages/raystack/components/image/image.tsx b/packages/raystack/components/image/image.tsx index 57b5c04b9..ae9e5cff9 100644 --- a/packages/raystack/components/image/image.tsx +++ b/packages/raystack/components/image/image.tsx @@ -1,7 +1,7 @@ 'use client'; -import { type VariantProps, cva } from 'class-variance-authority'; -import { ImgHTMLAttributes } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { ComponentProps, SyntheticEvent } from 'react'; import styles from './image.module.css'; @@ -25,12 +25,8 @@ const image = cva(styles.image, { } }); -interface ImageProps - extends ImgHTMLAttributes, - VariantProps { +interface ImageProps extends ComponentProps<'img'>, VariantProps { fallback?: string; - width?: string | number; - height?: string | number; } export function Image({ @@ -47,9 +43,7 @@ export function Image({ decoding = 'async', ...props }: ImageProps) { - const handleError = ( - event: React.SyntheticEvent - ) => { + const handleError = (event: SyntheticEvent) => { if (fallback) { event.currentTarget.src = fallback; } diff --git a/packages/raystack/components/indicator/indicator.tsx b/packages/raystack/components/indicator/indicator.tsx index 8fbc0a660..d78f82688 100644 --- a/packages/raystack/components/indicator/indicator.tsx +++ b/packages/raystack/components/indicator/indicator.tsx @@ -1,61 +1,55 @@ -import { cva, VariantProps } from "class-variance-authority"; -import { ComponentPropsWithoutRef, ReactNode } from "react"; +import { cva, VariantProps } from 'class-variance-authority'; +import { ComponentProps, ReactNode } from 'react'; import styles from './indicator.module.css'; const indicator = cva(styles.indicator, { variants: { variant: { - accent: styles["indicator-variant-accent"], - warning: styles["indicator-variant-warning"], - danger: styles["indicator-variant-danger"], - success: styles["indicator-variant-success"], - neutral: styles["indicator-variant-neutral"], + accent: styles['indicator-variant-accent'], + warning: styles['indicator-variant-warning'], + danger: styles['indicator-variant-danger'], + success: styles['indicator-variant-success'], + neutral: styles['indicator-variant-neutral'] } }, defaultVariants: { - variant: "accent", - }, + variant: 'accent' + } }); export interface IndicatorProps - extends ComponentPropsWithoutRef<"div">, + extends ComponentProps<'div'>, VariantProps { label?: string; children?: ReactNode; - "aria-label"?: string; + 'aria-label'?: string; } -export const Indicator = ({ - className, - variant, - label, - children, - "aria-label": ariaLabel, - ...props +export const Indicator = ({ + className, + variant, + label, + children, + 'aria-label': ariaLabel, + ...props }: IndicatorProps) => { const accessibilityLabel = ariaLabel || label || `${variant} indicator`; return (
    {children} -
    {label ? ( - + {label} ) : ( -
    diff --git a/packages/raystack/components/input-field/input-field.tsx b/packages/raystack/components/input-field/input-field.tsx index d5b5847b3..026c29931 100644 --- a/packages/raystack/components/input-field/input-field.tsx +++ b/packages/raystack/components/input-field/input-field.tsx @@ -2,12 +2,7 @@ import { InfoCircledIcon } from '@radix-ui/react-icons'; import { cva, cx, type VariantProps } from 'class-variance-authority'; -import { - ComponentPropsWithoutRef, - forwardRef, - ReactNode, - RefObject -} from 'react'; +import { ComponentProps, ReactNode, RefObject } from 'react'; import { Chip } from '../chip'; import { Tooltip } from '../tooltip'; import styles from './input-field.module.css'; @@ -32,7 +27,7 @@ const inputWrapper = cva(styles.inputWrapper, { }); export interface InputFieldProps - extends Omit, 'error' | 'size'>, + extends Omit, 'error' | 'size'>, VariantProps { label?: string; helperText?: string; @@ -51,124 +46,118 @@ export interface InputFieldProps containerRef?: RefObject; } -export const InputField = forwardRef( - ( - { - className, - disabled, - label, - helperText, - placeholder, - error, - leadingIcon, - trailingIcon, - optional, - prefix, - suffix, - width, - chips, - maxChipsVisible = 2, - size, - infoTooltip, - variant = 'default', - containerRef, - ...props - }, - ref - ) => { - return ( -
    - {label && ( -
    - - {infoTooltip && ( - - - - - } - /> - {infoTooltip} - - )} -
    - )} -
    - {leadingIcon && ( -
    {leadingIcon}
    +export function InputField({ + className, + disabled, + label, + helperText, + placeholder, + error, + leadingIcon, + trailingIcon, + optional, + prefix, + suffix, + width, + chips, + maxChipsVisible = 2, + size, + infoTooltip, + variant = 'default', + containerRef, + ...props +}: InputFieldProps) { + return ( +
    + {label && ( +
    + + {infoTooltip && ( + + + + + } + /> + {infoTooltip} + )} - {prefix &&
    {prefix}
    } - -
    - {chips?.slice(0, maxChipsVisible).map((chip, index) => ( - - {chip.label} - - ))} - {chips && chips.length > maxChipsVisible && ( - - +{chips.length - maxChipsVisible} - - )} - -
    +
    + )} +
    + {leadingIcon && ( +
    {leadingIcon}
    + )} + {prefix &&
    {prefix}
    } - {suffix &&
    {suffix}
    } - {trailingIcon && ( -
    {trailingIcon}
    +
    + {chips?.slice(0, maxChipsVisible).map((chip, index) => ( + + {chip.label} + + ))} + {chips && chips.length > maxChipsVisible && ( + + +{chips.length - maxChipsVisible} + )} -
    - {(error || helperText) && ( - - {error || helperText} - + aria-invalid={!!error} + placeholder={placeholder} + disabled={disabled} + {...props} + /> +
    + + {suffix &&
    {suffix}
    } + {trailingIcon && ( +
    {trailingIcon}
    )}
    - ); - } -); + {(error || helperText) && ( + + {error || helperText} + + )} +
    + ); +} InputField.displayName = 'InputField'; diff --git a/packages/raystack/components/label/label.tsx b/packages/raystack/components/label/label.tsx index 40c8e59eb..e50f36959 100644 --- a/packages/raystack/components/label/label.tsx +++ b/packages/raystack/components/label/label.tsx @@ -1,40 +1,41 @@ -import { cva, type VariantProps } from "class-variance-authority"; -import { HTMLAttributes, PropsWithChildren } from "react"; +import { cva, type VariantProps } from 'class-variance-authority'; +import { ComponentProps, PropsWithChildren } from 'react'; -import styles from "./label.module.css"; +import styles from './label.module.css'; const label = cva(styles.label, { variants: { size: { - small: styles["label-small"], - medium: styles["label-medium"], - large: styles["label-large"], + small: styles['label-small'], + medium: styles['label-medium'], + large: styles['label-large'] }, required: { - true: styles["label-required"], + true: styles['label-required'] } }, defaultVariants: { - size: "small", - required: false, - }, + size: 'small', + required: false + } }); -interface LabelProps extends PropsWithChildren>, - Omit, 'required'> { +interface LabelProps + extends PropsWithChildren>, + Omit, 'required'> { required?: boolean; requiredIndicator?: string; htmlFor?: string; } -export function Label({ - children, - className, - size, - required, - requiredIndicator = "*", +export function Label({ + children, + className, + size, + required, + requiredIndicator = '*', htmlFor, - ...props + ...props }: LabelProps) { return (
    +export function Cell({ + className, + children, + leadingIcon, + trailingIcon, + type = 'item', + ...props +}: CellProps) { + return ( +
    {leadingIcon && {leadingIcon}} {children} {trailingIcon && ( {trailingIcon} )}
    - ) -); + ); +} Cell.displayName = 'Menu.Cell'; diff --git a/packages/raystack/components/menu/menu-content.tsx b/packages/raystack/components/menu/menu-content.tsx index 0f2505d84..5b508e474 100644 --- a/packages/raystack/components/menu/menu-content.tsx +++ b/packages/raystack/components/menu/menu-content.tsx @@ -5,7 +5,7 @@ import { Menu as MenuPrimitive } from '@base-ui/react'; import { cx } from 'class-variance-authority'; -import { forwardRef, KeyboardEvent, useCallback, useRef } from 'react'; +import { KeyboardEvent, useCallback, useRef } from 'react'; import styles from './menu.module.css'; import { useMenuContext } from './menu-root'; import { @@ -24,76 +24,58 @@ export interface MenuContentProps searchPlaceholder?: string; } -export const MenuContent = forwardRef( - ( - { - className, - children, - searchPlaceholder = 'Search...', - render, - finalFocus, - style, - sideOffset = 4, - align = 'start', - onFocus, - ...positionerProps - }, - ref - ) => { - const { - autocomplete, - inputValue, - onInputValueChange, - inputRef, - isInitialRender, - parent - } = useMenuContext(); +export function MenuContent({ + ref, + className, + children, + searchPlaceholder = 'Search...', + render, + finalFocus, + style, + sideOffset = 4, + align = 'start', + onFocus, + ...positionerProps +}: MenuContentProps) { + const { + autocomplete, + inputValue, + onInputValueChange, + inputRef, + isInitialRender, + parent + } = useMenuContext(); + + const focusInput = useCallback(() => { + if (document?.activeElement !== inputRef?.current) + inputRef?.current?.focus(); + }, [inputRef]); + const highlightedItem = useRef< + [index: number, reason: 'keyboard' | 'pointer' | 'none'] + >([-1, 'none']); + const containerRef = useRef(null); - const focusInput = useCallback(() => { - if (document?.activeElement !== inputRef?.current) - inputRef?.current?.focus(); - }, [inputRef]); - const highlightedItem = useRef< - [index: number, reason: 'keyboard' | 'pointer' | 'none'] - >([-1, 'none']); - const containerRef = useRef(null); + const highlightFirstItem = useCallback(() => { + if (!isInitialRender?.current) return; + isInitialRender.current = false; + const item = containerRef.current?.querySelector('[role="option"]'); + if (!item) return; + item.dispatchEvent(new PointerEvent('mousemove', { bubbles: true })); + }, [isInitialRender]); - const highlightFirstItem = useCallback(() => { - if (!isInitialRender?.current) return; - isInitialRender.current = false; - const item = containerRef.current?.querySelector('[role="option"]'); - if (!item) return; - item.dispatchEvent(new PointerEvent('mousemove', { bubbles: true })); - }, [isInitialRender]); + const checkAndOpenSubMenu = useCallback(() => { + if (highlightedItem.current[0] === -1) return; + const items = containerRef.current?.querySelectorAll('[role="option"]'); + const item = items?.[highlightedItem.current[0]]; + if (!item || !isElementSubMenuTrigger(item)) return; + dispatchKeyboardEvent(item, KEYCODES.ARROW_RIGHT); + }, []); - const checkAndOpenSubMenu = useCallback(() => { + const checkAndCloseSubMenu = useCallback( + (e: KeyboardEvent) => { if (highlightedItem.current[0] === -1) return; const items = containerRef.current?.querySelectorAll('[role="option"]'); const item = items?.[highlightedItem.current[0]]; - if (!item || !isElementSubMenuTrigger(item)) return; - dispatchKeyboardEvent(item, KEYCODES.ARROW_RIGHT); - }, []); - - const checkAndCloseSubMenu = useCallback( - (e: KeyboardEvent) => { - if (highlightedItem.current[0] === -1) return; - const items = containerRef.current?.querySelectorAll('[role="option"]'); - const item = items?.[highlightedItem.current[0]]; - if ( - !item || - !isElementSubMenuTrigger(item) || - !isElementSubMenuOpen(item) - ) - return; - dispatchKeyboardEvent(item, KEYCODES.ESCAPE); - e.stopPropagation(); - }, - [] - ); - - const blurStaleMenuItem = useCallback((index: number) => { - const items = containerRef.current?.querySelectorAll('[role="option"]'); - const item = items?.[index]; if ( !item || !isElementSubMenuTrigger(item) || @@ -101,97 +83,107 @@ export const MenuContent = forwardRef( ) return; dispatchKeyboardEvent(item, KEYCODES.ESCAPE); - item.dispatchEvent(new PointerEvent('pointerout', { bubbles: true })); - }, []); + e.stopPropagation(); + }, + [] + ); + + const blurStaleMenuItem = useCallback((index: number) => { + const items = containerRef.current?.querySelectorAll('[role="option"]'); + const item = items?.[index]; + if (!item || !isElementSubMenuTrigger(item) || !isElementSubMenuOpen(item)) + return; + dispatchKeyboardEvent(item, KEYCODES.ESCAPE); + item.dispatchEvent(new PointerEvent('pointerout', { bubbles: true })); + }, []); - return ( - - + + { + focusInput(); + e.stopPropagation(); + highlightFirstItem(); + onFocus?.(e); + } + : undefined + } > - { - focusInput(); - e.stopPropagation(); - highlightFirstItem(); - onFocus?.(e); - } - : undefined - } - > - {autocomplete ? ( - onInputValueChange?.(value)} - autoHighlight={!!inputValue?.length} - mode='none' - loopFocus={false} - onItemHighlighted={(value, eventDetails) => { - if ( - highlightedItem.current[1] === 'pointer' && - eventDetails.reason === 'keyboard' - ) { - // focus moved using keyboard after using pointer - blurStaleMenuItem(highlightedItem.current[0]); - } - highlightedItem.current = [ - eventDetails.index, - eventDetails.reason - ]; + {autocomplete ? ( + onInputValueChange?.(value)} + autoHighlight={!!inputValue?.length} + mode='none' + loopFocus={false} + onItemHighlighted={(value, eventDetails) => { + if ( + highlightedItem.current[1] === 'pointer' && + eventDetails.reason === 'keyboard' + ) { + // focus moved using keyboard after using pointer + blurStaleMenuItem(highlightedItem.current[0]); + } + highlightedItem.current = [ + eventDetails.index, + eventDetails.reason + ]; + }} + > + { + focusInput(); + }} + onKeyDown={e => { + if (e.key === 'ArrowLeft') return; + if (e.key === 'Escape') return checkAndCloseSubMenu(e); + if (e.key === 'ArrowRight' || e.key === 'Enter') + checkAndOpenSubMenu(); + e.stopPropagation(); }} + tabIndex={-1} + /> + - { - focusInput(); - }} - onKeyDown={e => { - if (e.key === 'ArrowLeft') return; - if (e.key === 'Escape') return checkAndCloseSubMenu(e); - if (e.key === 'ArrowRight' || e.key === 'Enter') - checkAndOpenSubMenu(); - e.stopPropagation(); - }} - tabIndex={-1} - /> - - {children} - - - ) : ( - children - )} - - - - ); - } -); + {children} + + + ) : ( + children + )} + + + + ); +} MenuContent.displayName = 'Menu.Content'; -export const MenuSubContent = forwardRef( - ({ ...props }, ref) => -); -MenuSubContent.displayName = 'Menu.SubContent'; +export function MenuSubContent(props: MenuContentProps) { + return ; +} +MenuSubContent.displayName = 'Menu.SubmenuContent'; diff --git a/packages/raystack/components/menu/menu-item.tsx b/packages/raystack/components/menu/menu-item.tsx index 546b66f6c..27c9d011c 100644 --- a/packages/raystack/components/menu/menu-item.tsx +++ b/packages/raystack/components/menu/menu-item.tsx @@ -4,7 +4,6 @@ import { Autocomplete as AutocompletePrimitive, Menu as MenuPrimitive } from '@base-ui/react'; -import { forwardRef } from 'react'; import { Cell, CellBaseProps } from './cell'; import { useMenuContext } from './menu-root'; import { getMatch } from './utils'; @@ -13,46 +12,49 @@ export interface MenuItemProps extends MenuPrimitive.Item.Props, CellBaseProps { value?: string; } -export const MenuItem = forwardRef( - ({ children, value, leadingIcon, trailingIcon, render, ...props }, ref) => { - const { autocomplete, inputValue, shouldFilter } = useMenuContext(); +export function MenuItem({ + children, + value, + leadingIcon, + trailingIcon, + render, + ...props +}: MenuItemProps) { + const { autocomplete, inputValue, shouldFilter } = useMenuContext(); - const cell = render ?? ( - - ); - - // In auto mode, hide items that don't match the search value - if (shouldFilter && !getMatch(value, children, inputValue)) { - return null; - } + const cell = render ?? ( + + ); - if (autocomplete) { - return ( - } - {...props} - > - {children} - - ); - } + // In auto mode, hide items that don't match the search value + if (shouldFilter && !getMatch(value, children, inputValue)) { + return null; + } + if (autocomplete) { return ( - } {...props} - onFocus={e => { - e.stopPropagation(); - e.preventDefault(); - e.preventBaseUIHandler(); - }} > {children} - + ); } -); + + return ( + { + e.stopPropagation(); + e.preventDefault(); + e.preventBaseUIHandler(); + }} + > + {children} + + ); +} MenuItem.displayName = 'Menu.Item'; diff --git a/packages/raystack/components/menu/menu-misc.tsx b/packages/raystack/components/menu/menu-misc.tsx index 929f8b80c..1072642b5 100644 --- a/packages/raystack/components/menu/menu-misc.tsx +++ b/packages/raystack/components/menu/menu-misc.tsx @@ -2,31 +2,33 @@ import { Menu as MenuPrimitive } from '@base-ui/react/menu'; import { cx } from 'class-variance-authority'; -import { Fragment, forwardRef, HTMLAttributes, ReactNode } from 'react'; +import { ComponentProps, Fragment, ReactNode } from 'react'; import styles from './menu.module.css'; import { useMenuContext } from './menu-root'; -export const MenuGroup = forwardRef( - ({ className, children, ...props }, ref) => { - const { shouldFilter } = useMenuContext(); - - if (shouldFilter) { - return {children}; - } +export function MenuGroup({ + className, + children, + ...props +}: MenuPrimitive.Group.Props) { + const { shouldFilter } = useMenuContext(); - return ( - - {children} - - ); + if (shouldFilter) { + return {children}; } -); + + return ( + + {children} + + ); +} MenuGroup.displayName = 'Menu.Group'; -export const MenuLabel = forwardRef< - HTMLDivElement, - MenuPrimitive.GroupLabel.Props ->(({ className, ...props }, ref) => { +export function MenuLabel({ + className, + ...props +}: MenuPrimitive.GroupLabel.Props) { const { shouldFilter } = useMenuContext(); if (shouldFilter) { @@ -35,18 +37,14 @@ export const MenuLabel = forwardRef< return ( ); -}); +} MenuLabel.displayName = 'Menu.Label'; -export const MenuSeparator = forwardRef< - HTMLDivElement, - HTMLAttributes ->(({ className, ...props }, ref) => { +export function MenuSeparator({ className, ...props }: ComponentProps<'div'>) { const { shouldFilter } = useMenuContext(); if (shouldFilter) { @@ -55,23 +53,23 @@ export const MenuSeparator = forwardRef< return (
    ); -}); +} MenuSeparator.displayName = 'Menu.Separator'; -export const MenuEmptyState = forwardRef< - HTMLDivElement, - HTMLAttributes & { - children: ReactNode; - } ->(({ className, children, ...props }, ref) => ( -
    - {children} -
    -)); +export function MenuEmptyState({ + className, + children, + ...props +}: ComponentProps<'div'> & { children: ReactNode }) { + return ( +
    + {children} +
    + ); +} MenuEmptyState.displayName = 'Menu.EmptyState'; diff --git a/packages/raystack/components/menu/menu-root.tsx b/packages/raystack/components/menu/menu-root.tsx index 878cce912..3af2ef47e 100644 --- a/packages/raystack/components/menu/menu-root.tsx +++ b/packages/raystack/components/menu/menu-root.tsx @@ -135,7 +135,7 @@ export const MenuRoot = ({ ); return ( - - + ); }; MenuRoot.displayName = 'Menu'; @@ -224,7 +224,7 @@ export const MenuSubMenu = ({ ); return ( - - + ); }; -MenuSubMenu.displayName = 'Menu.SubMenu'; +MenuSubMenu.displayName = 'Menu.Submenu'; diff --git a/packages/raystack/components/menu/menu-trigger.tsx b/packages/raystack/components/menu/menu-trigger.tsx index 1c2f6b230..3c8d2f42f 100644 --- a/packages/raystack/components/menu/menu-trigger.tsx +++ b/packages/raystack/components/menu/menu-trigger.tsx @@ -2,7 +2,6 @@ import { Autocomplete as AutocompletePrimitive } from '@base-ui/react'; import { Menu as MenuPrimitive } from '@base-ui/react/menu'; -import { forwardRef } from 'react'; import { TriangleRightIcon } from '~/icons'; import { Button } from '../button'; import { useMenubarContext } from '../menubar/menubar'; @@ -15,33 +14,36 @@ export interface MenuTriggerProps extends MenuPrimitive.Trigger.Props { stopPropagation?: boolean; } -export const MenuTrigger = forwardRef( - ({ children, stopPropagation = true, onClick, render, ...props }, ref) => { - const inMenubarContext = useMenubarContext(); - const menubarRender = inMenubarContext ? ( -