From a48d005018f0340d0310ebaafce80f37f7dd34e7 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 10 Feb 2026 00:48:14 +0400 Subject: [PATCH 1/9] Emit on component rather than window --- spec/components/Carousel/Carousel.test.tsx | 60 ++++++++-- .../product-card/product-card.test.tsx | 113 +++++++++++++----- spec/utils/events.test.ts | 59 ++++++++- src/components/card.tsx | 36 ++++-- src/components/carousel.tsx | 69 ++++++++--- src/components/product-card.tsx | 90 +++++++++----- src/types/carouselTypes.ts | 3 +- src/utils/events.ts | 14 ++- 8 files changed, 344 insertions(+), 100 deletions(-) diff --git a/spec/components/Carousel/Carousel.test.tsx b/spec/components/Carousel/Carousel.test.tsx index 5819976..0011d29 100644 --- a/spec/components/Carousel/Carousel.test.tsx +++ b/spec/components/Carousel/Carousel.test.tsx @@ -603,11 +603,13 @@ describe('Carousel component', () => { cleanup(); }); - test('dispatches carousel.next event when next button is clicked', () => { + test('dispatches carousel.next event on root element when next button is clicked', () => { + const ref = React.createRef(); + render(); + const listener = vi.fn(); - window.addEventListener(CIO_EVENTS.carousel.next, listener); + ref.current!.addEventListener(CIO_EVENTS.carousel.next, listener); - render(); const nextButton = screen.getByRole('button', { name: /next/i }); fireEvent.click(nextButton); @@ -617,14 +619,16 @@ describe('Carousel component', () => { expect(typeof event.detail.canScrollNext).toBe('boolean'); expect(typeof event.detail.canScrollPrev).toBe('boolean'); - window.removeEventListener(CIO_EVENTS.carousel.next, listener); + ref.current!.removeEventListener(CIO_EVENTS.carousel.next, listener); }); - test('dispatches carousel.previous event when previous button is clicked', () => { + test('dispatches carousel.previous event on root element when previous button is clicked', () => { + const ref = React.createRef(); + render(); + const listener = vi.fn(); - window.addEventListener(CIO_EVENTS.carousel.previous, listener); + ref.current!.addEventListener(CIO_EVENTS.carousel.previous, listener); - render(); const prevButton = screen.getByRole('button', { name: /previous/i }); fireEvent.click(prevButton); @@ -634,7 +638,20 @@ describe('Carousel component', () => { expect(typeof event.detail.canScrollNext).toBe('boolean'); expect(typeof event.detail.canScrollPrev).toBe('boolean'); - window.removeEventListener(CIO_EVENTS.carousel.previous, listener); + ref.current!.removeEventListener(CIO_EVENTS.carousel.previous, listener); + }); + + test('events bubble up so window listeners still work', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.carousel.next, listener); + + render(); + const nextButton = screen.getByRole('button', { name: /next/i }); + fireEvent.click(nextButton); + + expect(listener).toHaveBeenCalledTimes(1); + + window.removeEventListener(CIO_EVENTS.carousel.next, listener); }); test('carousel navigation still works alongside event dispatch', () => { @@ -650,5 +667,32 @@ describe('Carousel component', () => { // Products should still be rendered expect(screen.getByText('Product 1')).toBeInTheDocument(); }); + + test('two carousels: events do not cross-pollinate', () => { + const ref1 = React.createRef(); + const ref2 = React.createRef(); + + render( + <> + + + , + ); + + const listener1 = vi.fn(); + const listener2 = vi.fn(); + ref1.current!.addEventListener(CIO_EVENTS.carousel.next, listener1); + ref2.current!.addEventListener(CIO_EVENTS.carousel.next, listener2); + + // Click next on the first carousel only + const nextButtons = screen.getAllByRole('button', { name: /next/i }); + fireEvent.click(nextButtons[0]); + + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).not.toHaveBeenCalled(); + + ref1.current!.removeEventListener(CIO_EVENTS.carousel.next, listener1); + ref2.current!.removeEventListener(CIO_EVENTS.carousel.next, listener2); + }); }); }); diff --git a/spec/components/product-card/product-card.test.tsx b/spec/components/product-card/product-card.test.tsx index d4e1604..ecf5b55 100644 --- a/spec/components/product-card/product-card.test.tsx +++ b/spec/components/product-card/product-card.test.tsx @@ -400,45 +400,50 @@ describe('ProductCard component', () => { cleanup(); }); - test('dispatches productCard.click event on card click with correct product detail', () => { + test('dispatches productCard.click event on root element with correct product detail', () => { + const ref = React.createRef(); + render(); + const listener = vi.fn(); - window.addEventListener(CIO_EVENTS.productCard.click, listener); + ref.current!.addEventListener(CIO_EVENTS.productCard.click, listener); - render(); fireEvent.click(screen.getByTestId('product-card')); expect(listener).toHaveBeenCalledTimes(1); const event = listener.mock.calls[0][0] as CustomEvent; expect(event.detail.product).toEqual(mockProductData.product); - window.removeEventListener(CIO_EVENTS.productCard.click, listener); + ref.current!.removeEventListener(CIO_EVENTS.productCard.click, listener); }); test('dispatches productCard.click event AND calls onProductClick callback', () => { - const listener = vi.fn(); + const ref = React.createRef(); const mockOnProductClick = vi.fn(); - window.addEventListener(CIO_EVENTS.productCard.click, listener); - render( , ); + + const listener = vi.fn(); + ref.current!.addEventListener(CIO_EVENTS.productCard.click, listener); + fireEvent.click(screen.getByTestId('product-card')); expect(listener).toHaveBeenCalledTimes(1); expect(mockOnProductClick).toHaveBeenCalledTimes(1); - window.removeEventListener(CIO_EVENTS.productCard.click, listener); + ref.current!.removeEventListener(CIO_EVENTS.productCard.click, listener); }); - test('dispatches productCard.click event even without onProductClick prop', () => { + test('events bubble up so window listeners still work', () => { const listener = vi.fn(); window.addEventListener(CIO_EVENTS.productCard.click, listener); - render(); + render(); fireEvent.click(screen.getByTestId('product-card')); expect(listener).toHaveBeenCalledTimes(1); @@ -446,12 +451,28 @@ describe('ProductCard component', () => { window.removeEventListener(CIO_EVENTS.productCard.click, listener); }); - test('dispatches productCard.conversion event on add-to-cart click and calls onAddToCart', () => { + test('dispatches productCard.click event even without onProductClick prop', () => { + const ref = React.createRef(); + render(); + const listener = vi.fn(); + ref.current!.addEventListener(CIO_EVENTS.productCard.click, listener); + + fireEvent.click(screen.getByTestId('product-card')); + + expect(listener).toHaveBeenCalledTimes(1); + + ref.current!.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('dispatches productCard.conversion event on root element on add-to-cart click', () => { + const ref = React.createRef(); const mockOnAddToCart = vi.fn(); - window.addEventListener(CIO_EVENTS.productCard.conversion, listener); + render(); + + const listener = vi.fn(); + ref.current!.addEventListener(CIO_EVENTS.productCard.conversion, listener); - render(); fireEvent.click(screen.getByText('Add to Cart')); expect(listener).toHaveBeenCalledTimes(1); @@ -459,53 +480,87 @@ describe('ProductCard component', () => { expect(event.detail.product).toEqual(mockProductData.product); expect(mockOnAddToCart).toHaveBeenCalledTimes(1); - window.removeEventListener(CIO_EVENTS.productCard.conversion, listener); + ref.current!.removeEventListener(CIO_EVENTS.productCard.conversion, listener); }); test('clicking add-to-cart does NOT also dispatch productCard.click', () => { + const ref = React.createRef(); + render(); + const clickListener = vi.fn(); const conversionListener = vi.fn(); - window.addEventListener(CIO_EVENTS.productCard.click, clickListener); - window.addEventListener(CIO_EVENTS.productCard.conversion, conversionListener); + ref.current!.addEventListener(CIO_EVENTS.productCard.click, clickListener); + ref.current!.addEventListener(CIO_EVENTS.productCard.conversion, conversionListener); - render(); fireEvent.click(screen.getByText('Add to Cart')); expect(conversionListener).toHaveBeenCalledTimes(1); expect(clickListener).not.toHaveBeenCalled(); - window.removeEventListener(CIO_EVENTS.productCard.click, clickListener); - window.removeEventListener(CIO_EVENTS.productCard.conversion, conversionListener); + ref.current!.removeEventListener(CIO_EVENTS.productCard.click, clickListener); + ref.current!.removeEventListener(CIO_EVENTS.productCard.conversion, conversionListener); }); - test('dispatches productCard.imageEnter on mouseEnter of image section', () => { + test('dispatches productCard.imageEnter on root element on mouseEnter of image section', () => { + const ref = React.createRef(); + render(); + const listener = vi.fn(); - window.addEventListener(CIO_EVENTS.productCard.imageEnter, listener); + ref.current!.addEventListener(CIO_EVENTS.productCard.imageEnter, listener); - const { container } = render(); - const imageSection = container.querySelector('.cio-product-card-image-section')!; + const imageSection = ref.current!.querySelector('.cio-product-card-image-section')!; fireEvent.mouseEnter(imageSection); expect(listener).toHaveBeenCalledTimes(1); const event = listener.mock.calls[0][0] as CustomEvent; expect(event.detail.product).toEqual(mockProductData.product); - window.removeEventListener(CIO_EVENTS.productCard.imageEnter, listener); + ref.current!.removeEventListener(CIO_EVENTS.productCard.imageEnter, listener); }); - test('dispatches productCard.imageLeave on mouseLeave of image section', () => { + test('dispatches productCard.imageLeave on root element on mouseLeave of image section', () => { + const ref = React.createRef(); + render(); + const listener = vi.fn(); - window.addEventListener(CIO_EVENTS.productCard.imageLeave, listener); + ref.current!.addEventListener(CIO_EVENTS.productCard.imageLeave, listener); - const { container } = render(); - const imageSection = container.querySelector('.cio-product-card-image-section')!; + const imageSection = ref.current!.querySelector('.cio-product-card-image-section')!; fireEvent.mouseLeave(imageSection); expect(listener).toHaveBeenCalledTimes(1); const event = listener.mock.calls[0][0] as CustomEvent; expect(event.detail.product).toEqual(mockProductData.product); - window.removeEventListener(CIO_EVENTS.productCard.imageLeave, listener); + ref.current!.removeEventListener(CIO_EVENTS.productCard.imageLeave, listener); + }); + + test('two product cards: events do not cross-pollinate', () => { + const ref1 = React.createRef(); + const ref2 = React.createRef(); + + const product2 = { ...mockProductData, product: { ...mockProductData.product, id: 'product-2', name: 'Product 2' } }; + + render( + <> + + + , + ); + + const listener1 = vi.fn(); + const listener2 = vi.fn(); + ref1.current!.addEventListener(CIO_EVENTS.productCard.click, listener1); + ref2.current!.addEventListener(CIO_EVENTS.productCard.click, listener2); + + // Click only the first card + fireEvent.click(screen.getByTestId('card-1')); + + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).not.toHaveBeenCalled(); + + ref1.current!.removeEventListener(CIO_EVENTS.productCard.click, listener1); + ref2.current!.removeEventListener(CIO_EVENTS.productCard.click, listener2); }); }); }); diff --git a/spec/utils/events.test.ts b/spec/utils/events.test.ts index 213d353..ae61a76 100644 --- a/spec/utils/events.test.ts +++ b/spec/utils/events.test.ts @@ -70,7 +70,7 @@ describe('Event utility', () => { } }); - test('dispatches with bubbles false and cancelable true', () => { + test('dispatches with bubbles true and cancelable true', () => { const listener = vi.fn(); window.addEventListener(CIO_EVENTS.carousel.next, listener); @@ -81,10 +81,65 @@ describe('Event utility', () => { }); const event = listener.mock.calls[0][0] as CustomEvent; - expect(event.bubbles).toBe(false); + expect(event.bubbles).toBe(true); expect(event.cancelable).toBe(true); window.removeEventListener(CIO_EVENTS.carousel.next, listener); }); + + test('dispatches on a specific DOM element when target is provided', () => { + const element = document.createElement('div'); + document.body.appendChild(element); + const listener = vi.fn(); + element.addEventListener(CIO_EVENTS.productCard.click, listener); + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }, element); + + expect(listener).toHaveBeenCalledTimes(1); + const event = listener.mock.calls[0][0] as CustomEvent; + expect(event.detail.product).toEqual(mockProduct); + + element.removeEventListener(CIO_EVENTS.productCard.click, listener); + document.body.removeChild(element); + }); + + test('event bubbles from child element to parent listener', () => { + const parent = document.createElement('div'); + const child = document.createElement('span'); + parent.appendChild(child); + document.body.appendChild(parent); + + const parentListener = vi.fn(); + parent.addEventListener(CIO_EVENTS.productCard.click, parentListener); + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }, child); + + expect(parentListener).toHaveBeenCalledTimes(1); + + parent.removeEventListener(CIO_EVENTS.productCard.click, parentListener); + document.body.removeChild(parent); + }); + + test('falls back to window when target is null', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.productCard.click, listener); + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }, null); + + expect(listener).toHaveBeenCalledTimes(1); + + window.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); + + test('falls back to window when target is undefined', () => { + const listener = vi.fn(); + window.addEventListener(CIO_EVENTS.productCard.click, listener); + + dispatchCioEvent(CIO_EVENTS.productCard.click, { product: mockProduct }, undefined); + + expect(listener).toHaveBeenCalledTimes(1); + + window.removeEventListener(CIO_EVENTS.productCard.click, listener); + }); }); }); diff --git a/src/components/card.tsx b/src/components/card.tsx index c37e358..cfd50e2 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, createContext, useContext } from 'react'; +import React, { ReactNode, createContext, useContext, forwardRef } from 'react'; import { cn, RenderPropsWrapper } from '@/utils'; import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; @@ -65,7 +65,10 @@ const useCardContext = (): CardContextType => { }; // Helper function to create the Card root -function Card({ children, componentOverrides, className, ...props }: CardProps) { +const Card = forwardRef(function Card( + { children, componentOverrides, className, ...props }, + ref, +) { const contextValue: CardContextType = React.useMemo( () => ({ renderProps: props, // Use merged props in context @@ -78,6 +81,7 @@ function Card({ children, componentOverrides, className, ...props }: CardProps)
); -} +}); function CardHeader({ children, className, ...props }: CardHeaderProps) { const { renderProps, componentOverrides } = useCardContext(); @@ -181,12 +185,20 @@ function CardFooter({ children, className, ...props }: CardFooterProps) { ); } -// Attach compound components to Card -Card.Header = CardHeader; -Card.Title = CardTitle; -Card.Description = CardDescription; -Card.Action = CardAction; -Card.Content = CardContent; -Card.Footer = CardFooter; - -export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; +// Attach compound sub-components to the forwardRef'd Card +const CardNamespace = Card as typeof Card & { + Header: typeof CardHeader; + Title: typeof CardTitle; + Description: typeof CardDescription; + Action: typeof CardAction; + Content: typeof CardContent; + Footer: typeof CardFooter; +}; +CardNamespace.Header = CardHeader; +CardNamespace.Title = CardTitle; +CardNamespace.Description = CardDescription; +CardNamespace.Action = CardAction; +CardNamespace.Content = CardContent; +CardNamespace.Footer = CardFooter; + +export { CardNamespace as Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/src/components/carousel.tsx b/src/components/carousel.tsx index 4dd72aa..a03febc 100644 --- a/src/components/carousel.tsx +++ b/src/components/carousel.tsx @@ -5,6 +5,8 @@ import React, { useState, useCallback, useEffect, + useRef, + forwardRef, } from 'react'; import useEmblaCarousel from 'embla-carousel-react'; import Autoplay from 'embla-carousel-autoplay'; @@ -61,8 +63,9 @@ function CarouselBase({ children, componentOverrides, plugins, + innerRef, ...props -}: ComponentProps<'div'> & CarouselProps) { +}: ComponentProps<'div'> & CarouselProps & { innerRef?: React.Ref }) { const { orientation = 'horizontal', autoPlay, @@ -83,6 +86,7 @@ function CarouselBase({ }, plugins, ); + const rootRef = useRef(null); const { rootProps } = useCarouselResponsive(responsive, orientation); useCarouselTweenOpacity(api, orientation); @@ -148,6 +152,7 @@ function CarouselBase({ }, componentOverrides: componentOverrides as CarouselOverrides | undefined, carouselRef, + rootRef, }; }, [ orientation, @@ -164,9 +169,24 @@ function CarouselBase({ carouselRef, ]); + const setRootRef = useCallback( + (node: HTMLDivElement | null) => { + // Set internal rootRef + (rootRef as React.MutableRefObject).current = node; + // Forward ref to consumer + if (typeof innerRef === 'function') { + innerRef(node); + } else if (innerRef) { + (innerRef as React.MutableRefObject).current = node; + } + }, + [innerRef], + ); + return (
({ ); } -function Carousel(props: CarouselOpts) { +function CarouselInner( + props: CarouselOpts, + ref: React.Ref, +) { const { children, items, componentOverrides, ...rest } = props; const { autoPlay, slidesToScroll, orientation, loop, responsive } = rest; @@ -197,6 +220,7 @@ function Carousel(props: CarouselOpts) { return ( (props: CarouselOpts) { ); } +// forwardRef wrapper that preserves generic support via type assertion +const Carousel = forwardRef(CarouselInner) as ( + props: CarouselOpts & { ref?: React.Ref }, +) => React.ReactElement | null; + function CarouselContent({ className, children, ...props }: ComponentProps<'div'>) { const { carouselRef, renderProps, componentOverrides } = useCarousel(); const { orientation } = renderProps; @@ -305,7 +334,7 @@ function CarouselNavButton({ className, ...props }: NavButtonProps & { direction: CarouselDirection }) { - const { renderProps, componentOverrides } = useCarousel(); + const { renderProps, componentOverrides, rootRef } = useCarousel(); const { canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation } = renderProps; const isPrevious = direction === 'previous'; @@ -315,14 +344,18 @@ function CarouselNavButton({ const handleClick = useCallback(() => { const eventName = isPrevious ? CIO_EVENTS.carousel.previous : CIO_EVENTS.carousel.next; - dispatchCioEvent(eventName, { - direction, - canScrollNext, - canScrollPrev, - }); + dispatchCioEvent( + eventName, + { + direction, + canScrollNext: canScrollNext ?? false, + canScrollPrev: canScrollPrev ?? false, + }, + rootRef.current, + ); scrollFn?.(); - }, [isPrevious, direction, canScrollNext, canScrollPrev, scrollFn]); + }, [isPrevious, direction, canScrollNext, canScrollPrev, scrollFn, rootRef]); const override = isPrevious ? componentOverrides?.previous?.reactNode @@ -360,10 +393,16 @@ function CarouselNext(props: NavButtonProps) { return ; } -// Create compound component with all sub-components attached -Carousel.Content = CarouselContent; -Carousel.Item = CarouselItem; -Carousel.Previous = CarouselPrevious; -Carousel.Next = CarouselNext; +// Attach compound sub-components to the forwardRef'd Carousel +const CarouselNamespace = Carousel as typeof Carousel & { + Content: typeof CarouselContent; + Item: typeof CarouselItem; + Previous: typeof CarouselPrevious; + Next: typeof CarouselNext; +}; +CarouselNamespace.Content = CarouselContent; +CarouselNamespace.Item = CarouselItem; +CarouselNamespace.Previous = CarouselPrevious; +CarouselNamespace.Next = CarouselNext; -export default Carousel; +export default CarouselNamespace; diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx index 31b3ed7..c06c0be 100644 --- a/src/components/product-card.tsx +++ b/src/components/product-card.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useCallback, useContext } from 'react'; +import React, { createContext, useCallback, useContext, useRef, forwardRef } from 'react'; import { cn, RenderPropsWrapper, dispatchCioEvent, CIO_EVENTS } from '@/utils'; import { Card, CardContentProps, CardFooterProps } from '@/components/card'; import Button from '@/components/button'; @@ -25,6 +25,7 @@ import { interface ProductCardContextValue { renderProps: Omit; componentOverrides?: ProductCardOverrides; + rootRef: React.RefObject; } const ProductCardContext = createContext(null); @@ -154,19 +155,19 @@ const TagsSection: React.FC = (props) => { }; const ImageSection: React.FC = (props) => { - const { renderProps, componentOverrides } = useProductCardContext(); + const { renderProps, componentOverrides, rootRef } = useProductCardContext(); const { imageUrl: contextImageUrl, name } = renderProps.product; // Use props with fallback to context values const imageUrl = props.imageUrl || contextImageUrl; const handleMouseEnter = useCallback(() => { - dispatchCioEvent(CIO_EVENTS.productCard.imageEnter, { product: renderProps.product }); - }, [renderProps.product]); + dispatchCioEvent(CIO_EVENTS.productCard.imageEnter, { product: renderProps.product }, rootRef.current); + }, [renderProps.product, rootRef]); const handleMouseLeave = useCallback(() => { - dispatchCioEvent(CIO_EVENTS.productCard.imageLeave, { product: renderProps.product }); - }, [renderProps.product]); + dispatchCioEvent(CIO_EVENTS.productCard.imageLeave, { product: renderProps.product }, rootRef.current); + }, [renderProps.product, rootRef]); return ( @@ -232,7 +233,7 @@ const DescriptionSection: React.FC = (props) => { }; const AddToCartButton: React.FC = (props) => { - const { renderProps, componentOverrides } = useProductCardContext(); + const { renderProps, componentOverrides, rootRef } = useProductCardContext(); const { addToCartText = renderProps.addToCartText || 'Add to Cart', onAddToCart = renderProps.onAddToCart, @@ -243,10 +244,10 @@ const AddToCartButton: React.FC = (props) => { (e: React.MouseEvent) => { // Prevent product click from firing e.stopPropagation(); - dispatchCioEvent(CIO_EVENTS.productCard.conversion, { product: renderProps.product }); + dispatchCioEvent(CIO_EVENTS.productCard.conversion, { product: renderProps.product }, rootRef.current); onAddToCart?.(e); }, - [renderProps.product, onAddToCart], + [renderProps.product, onAddToCart, rootRef], ); return ( @@ -350,11 +351,29 @@ function getProductCardDataAttributes({ }; } -function ProductCard({ componentOverrides, children, className, ...props }: ProductCardProps) { +const ProductCard = forwardRef(function ProductCard( + { componentOverrides, children, className, ...props }, + ref, +) { + const rootRef = useRef(null); + + const setRootRef = useCallback( + (node: HTMLDivElement | null) => { + (rootRef as React.MutableRefObject).current = node; + if (typeof ref === 'function') { + ref(node); + } else if (ref) { + (ref as React.MutableRefObject).current = node; + } + }, + [ref], + ); + const contextValue = React.useMemo( () => ({ renderProps: { ...props, ...getProductCardDataAttributes(props.product) }, componentOverrides, + rootRef, }), [props, componentOverrides], ); @@ -372,7 +391,7 @@ function ProductCard({ componentOverrides, children, className, ...props }: Prod } = props; const handleProductClick = useCallback(() => { - dispatchCioEvent(CIO_EVENTS.productCard.click, { product }); + dispatchCioEvent(CIO_EVENTS.productCard.click, { product }, rootRef.current); onProductClick?.(); }, [product, onProductClick]); @@ -383,6 +402,7 @@ function ProductCard({ componentOverrides, children, className, ...props }: Prod ); -} - -// Create compound component with all sub-components attached -ProductCard.ImageSection = ImageSection; -ProductCard.Badge = Badge; -ProductCard.WishlistButton = WishlistButton; -ProductCard.PriceSection = PriceSection; -ProductCard.TitleSection = TitleSection; -ProductCard.DescriptionSection = DescriptionSection; -ProductCard.RatingSection = RatingSection; -ProductCard.TagsSection = TagsSection; -ProductCard.AddToCartButton = AddToCartButton; -ProductCard.Content = ProductCardContent; -ProductCard.Footer = ProductCardFooter; -ProductCard.getProductCardDataAttributes = getProductCardDataAttributes; - -export default ProductCard; +}); + +// Attach compound sub-components to the forwardRef'd ProductCard +const ProductCardNamespace = ProductCard as typeof ProductCard & { + ImageSection: typeof ImageSection; + Badge: typeof Badge; + WishlistButton: typeof WishlistButton; + PriceSection: typeof PriceSection; + TitleSection: typeof TitleSection; + DescriptionSection: typeof DescriptionSection; + RatingSection: typeof RatingSection; + TagsSection: typeof TagsSection; + AddToCartButton: typeof AddToCartButton; + Content: typeof ProductCardContent; + Footer: typeof ProductCardFooter; + getProductCardDataAttributes: typeof getProductCardDataAttributes; +}; +ProductCardNamespace.ImageSection = ImageSection; +ProductCardNamespace.Badge = Badge; +ProductCardNamespace.WishlistButton = WishlistButton; +ProductCardNamespace.PriceSection = PriceSection; +ProductCardNamespace.TitleSection = TitleSection; +ProductCardNamespace.DescriptionSection = DescriptionSection; +ProductCardNamespace.RatingSection = RatingSection; +ProductCardNamespace.TagsSection = TagsSection; +ProductCardNamespace.AddToCartButton = AddToCartButton; +ProductCardNamespace.Content = ProductCardContent; +ProductCardNamespace.Footer = ProductCardFooter; +ProductCardNamespace.getProductCardDataAttributes = getProductCardDataAttributes; + +export default ProductCardNamespace; diff --git a/src/types/carouselTypes.ts b/src/types/carouselTypes.ts index db2a0a0..75b8aed 100644 --- a/src/types/carouselTypes.ts +++ b/src/types/carouselTypes.ts @@ -4,7 +4,7 @@ import type { IncludeComponentOverrides, RenderPropsChildren, } from '@/types'; -import type { ComponentProps, ReactNode } from 'react'; +import type { ComponentProps, ReactNode, RefObject } from 'react'; import type { UseEmblaCarouselType } from 'embla-carousel-react'; import type useEmblaCarousel from 'embla-carousel-react'; import type Button from '@/components/button'; @@ -33,6 +33,7 @@ export type CarouselProps = { export type CarouselContextValue = { carouselRef: ReturnType[0]; + rootRef: RefObject; renderProps: CarouselRenderProps; componentOverrides?: CarouselOverrides; } & CarouselProps; diff --git a/src/utils/events.ts b/src/utils/events.ts index 353f4dc..98b0f78 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -36,25 +36,29 @@ export interface CioEventDetailMap { } /** - * Dispatches a typed CustomEvent on `window` for the given CIO event name. + * Dispatches a typed CustomEvent for the given CIO event name. * * This is the primary pub-sub mechanism for Constructor.io UI component events. - * Consumers subscribe via `window.addEventListener(CIO_EVENTS.*.*, handler)`. - * No-ops in SSR environments where `window` is undefined. + * When a `target` element is provided, the event is dispatched on that element + * and bubbles up the DOM tree. Consumers can listen on the component element or + * any ancestor. When no target is provided (or during SSR), falls back to + * dispatching on `window` for backwards compatibility. * * @param eventName - A key from {@link CioEventDetailMap} (use `CIO_EVENTS` constants). * @param detail - The strongly-typed payload for the event. + * @param target - Optional DOM element to dispatch the event on. Falls back to `window`. */ export function dispatchCioEvent( eventName: K, detail: CioEventDetailMap[K], + target?: EventTarget | null, ): void { if (typeof window === 'undefined') return; const event = new CustomEvent(eventName, { - bubbles: false, + bubbles: true, cancelable: true, detail, }); - window.dispatchEvent(event); + (target || window).dispatchEvent(event); } From b1e0fa93219300317cdd033fd3f65df46e85cf06 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 10 Feb 2026 14:42:26 +0400 Subject: [PATCH 2/9] Lint and types --- spec/utils/events.test.ts | 1 - src/components/card.tsx | 10 +++++++++- src/components/carousel.tsx | 5 +---- src/components/product-card.tsx | 18 +++++++++++++++--- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/spec/utils/events.test.ts b/spec/utils/events.test.ts index ae61a76..4fb011d 100644 --- a/spec/utils/events.test.ts +++ b/spec/utils/events.test.ts @@ -59,7 +59,6 @@ describe('Event utility', () => { test('no-ops without throwing when window is undefined (SSR)', () => { const originalWindow = globalThis.window; - // @ts-expect-error -- simulating SSR by removing window delete (globalThis as Record).window; try { expect(() => { diff --git a/src/components/card.tsx b/src/components/card.tsx index cfd50e2..a8f3d54 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -201,4 +201,12 @@ CardNamespace.Action = CardAction; CardNamespace.Content = CardContent; CardNamespace.Footer = CardFooter; -export { CardNamespace as Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; +export { + CardNamespace as Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/src/components/carousel.tsx b/src/components/carousel.tsx index a03febc..adfe2ad 100644 --- a/src/components/carousel.tsx +++ b/src/components/carousel.tsx @@ -200,10 +200,7 @@ function CarouselBase({ ); } -function CarouselInner( - props: CarouselOpts, - ref: React.Ref, -) { +function CarouselInner(props: CarouselOpts, ref: React.Ref) { const { children, items, componentOverrides, ...rest } = props; const { autoPlay, slidesToScroll, orientation, loop, responsive } = rest; diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx index c06c0be..c7ab7d7 100644 --- a/src/components/product-card.tsx +++ b/src/components/product-card.tsx @@ -162,11 +162,19 @@ const ImageSection: React.FC = (props) => { const imageUrl = props.imageUrl || contextImageUrl; const handleMouseEnter = useCallback(() => { - dispatchCioEvent(CIO_EVENTS.productCard.imageEnter, { product: renderProps.product }, rootRef.current); + dispatchCioEvent( + CIO_EVENTS.productCard.imageEnter, + { product: renderProps.product }, + rootRef.current, + ); }, [renderProps.product, rootRef]); const handleMouseLeave = useCallback(() => { - dispatchCioEvent(CIO_EVENTS.productCard.imageLeave, { product: renderProps.product }, rootRef.current); + dispatchCioEvent( + CIO_EVENTS.productCard.imageLeave, + { product: renderProps.product }, + rootRef.current, + ); }, [renderProps.product, rootRef]); return ( @@ -244,7 +252,11 @@ const AddToCartButton: React.FC = (props) => { (e: React.MouseEvent) => { // Prevent product click from firing e.stopPropagation(); - dispatchCioEvent(CIO_EVENTS.productCard.conversion, { product: renderProps.product }, rootRef.current); + dispatchCioEvent( + CIO_EVENTS.productCard.conversion, + { product: renderProps.product }, + rootRef.current, + ); onAddToCart?.(e); }, [renderProps.product, onAddToCart, rootRef], From 7e1fbd5dd0a8f7a9dd3bf98b0f6702cfd3ee2b84 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 10 Feb 2026 14:49:17 +0400 Subject: [PATCH 3/9] add comments --- src/components/card.tsx | 3 ++- src/components/carousel.tsx | 4 +++- src/components/product-card.tsx | 2 ++ src/utils/events.ts | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/card.tsx b/src/components/card.tsx index a8f3d54..086fab3 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -64,7 +64,8 @@ const useCardContext = (): CardContextType => { return context; }; -// Helper function to create the Card root +// forwardRef so parent components (e.g. ProductCard) can attach a ref to the root div +// for dispatching scoped custom events on this element instead of window const Card = forwardRef(function Card( { children, componentOverrides, className, ...props }, ref, diff --git a/src/components/carousel.tsx b/src/components/carousel.tsx index adfe2ad..9af1cb2 100644 --- a/src/components/carousel.tsx +++ b/src/components/carousel.tsx @@ -259,7 +259,9 @@ function CarouselInner(props: CarouselOpts, ref: React.Ref( props: CarouselOpts & { ref?: React.Ref }, ) => React.ReactElement | null; diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx index c7ab7d7..b08144e 100644 --- a/src/components/product-card.tsx +++ b/src/components/product-card.tsx @@ -363,6 +363,8 @@ function getProductCardDataAttributes({ }; } +// forwardRef so consumers can access the root element to listen for scoped custom events +// (e.g. ref.current.addEventListener(CIO_EVENTS.productCard.click, handler)) const ProductCard = forwardRef(function ProductCard( { componentOverrides, children, className, ...props }, ref, diff --git a/src/utils/events.ts b/src/utils/events.ts index 98b0f78..b423efd 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -56,7 +56,7 @@ export function dispatchCioEvent( if (typeof window === 'undefined') return; const event = new CustomEvent(eventName, { - bubbles: true, + bubbles: true, // lets consumers listen on any ancestor, not just the dispatching element. cancelable: true, detail, }); From 02725af72a08f9816ba9fb555cc93ae8e5fa4571 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 10 Feb 2026 19:25:39 +0400 Subject: [PATCH 4/9] Add docs --- .../Carousel/CarouselEvents.stories.tsx | 90 ++++++++++++++++++ .../Carousel/Code Examples - Events.mdx | 77 ++++++++++++++++ .../ProductCard/Code Examples - Events.mdx | 72 +++++++++++++++ .../ProductCard/ProductCardEvents.stories.tsx | 92 +++++++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 src/stories/components/Carousel/CarouselEvents.stories.tsx create mode 100644 src/stories/components/Carousel/Code Examples - Events.mdx create mode 100644 src/stories/components/ProductCard/Code Examples - Events.mdx create mode 100644 src/stories/components/ProductCard/ProductCardEvents.stories.tsx diff --git a/src/stories/components/Carousel/CarouselEvents.stories.tsx b/src/stories/components/Carousel/CarouselEvents.stories.tsx new file mode 100644 index 0000000..8bec9ab --- /dev/null +++ b/src/stories/components/Carousel/CarouselEvents.stories.tsx @@ -0,0 +1,90 @@ +import React, { useRef, useEffect, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import CioCarousel from '../../../components/carousel'; +import { Product } from '../../../types/productCardTypes'; +import { CIO_EVENTS } from '../../../utils/events'; +import { DEMO_IMAGE_URL } from '../../constants'; + +const meta = { + title: 'Components/Carousel', + component: CioCarousel, + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mockProducts: Product[] = Array.from({ length: 10 }, (_, i) => ({ + id: `product-${i + 1}`, + name: `Product ${i + 1}`, + description: `This is a description for product ${i + 1}`, + imageUrl: DEMO_IMAGE_URL, + price: (Math.random() * 100 + 20).toFixed(2), + salePrice: Math.random() > 0.5 ? (Math.random() * 80 + 10).toFixed(2) : undefined, + rating: (Math.random() * 2 + 3).toFixed(1), + reviewsCount: Math.floor(Math.random() * 500 + 10), + tags: ['Tag 1', 'Tag 2'].slice(0, Math.floor(Math.random() * 3)), +})); + +function CarouselEventListeningDemo() { + const carouselRef = useRef(null); + const [eventLog, setEventLog] = useState([]); + + useEffect(() => { + const el = carouselRef.current; + if (!el) return; + + const logEvent = (label: string) => (e: Event) => { + const detail = (e as CustomEvent).detail; + setEventLog((prev) => [ + `[${new Date().toLocaleTimeString()}] ${label} — direction: ${detail?.direction}, canScrollNext: ${detail?.canScrollNext}, canScrollPrev: ${detail?.canScrollPrev}`, + ...prev.slice(0, 49), + ]); + }; + + const handlers = [ + [CIO_EVENTS.carousel.next, logEvent('carousel.next')] as const, + [CIO_EVENTS.carousel.previous, logEvent('carousel.previous')] as const, + ]; + + handlers.forEach(([name, fn]) => el.addEventListener(name, fn)); + return () => handlers.forEach(([name, fn]) => el.removeEventListener(name, fn)); + }, []); + + return ( +
+ +
+
Event Log
+ {eventLog.length === 0 ? ( +
+ Click the carousel arrows to see events... +
+ ) : ( + eventLog.map((entry, i) => ( +
+ {entry} +
+ )) + )} +
+
+ ); +} + +export const EventListening: Story = { + render: () => , + tags: ['!autodocs', '!dev'], +}; diff --git a/src/stories/components/Carousel/Code Examples - Events.mdx b/src/stories/components/Carousel/Code Examples - Events.mdx new file mode 100644 index 0000000..3c314c5 --- /dev/null +++ b/src/stories/components/Carousel/Code Examples - Events.mdx @@ -0,0 +1,77 @@ +import { Meta, Canvas } from '@storybook/addon-docs/blocks'; +import * as CarouselEventsStories from './CarouselEvents.stories'; + + + +# `Carousel` - Scoped Event Listening + +Listen for navigation events directly on a `Carousel` instance using a `ref`, instead of on `window`. + +## Why scoped events? + +When multiple `Carousel` instances exist on the same page, listening on `window` means **every** listener fires for **every** carousel. Scoped events solve this: each carousel dispatches events on its own root DOM element, so a listener attached via `ref` only receives events from that specific carousel. + +Events are dispatched with `bubbles: true`, so you can also listen on any ancestor element if you prefer a single handler for a group of carousels. + +## Interactive Demo + +Click the previous/next arrows to see navigation events appear in the log panel below. + + + +## Basic Setup + +```tsx +import { useRef, useEffect } from 'react'; +import CioCarousel from '@constructorio/ui-components/carousel'; +import { CIO_EVENTS } from '@constructorio/ui-components'; + +function MyCarousel({ products }) { + const carouselRef = useRef(null); + + useEffect(() => { + const el = carouselRef.current; + if (!el) return; + + const handleNav = (e: Event) => { + const { direction, canScrollNext, canScrollPrev } = (e as CustomEvent).detail; + console.log(`Scrolled ${direction}`, { canScrollNext, canScrollPrev }); + }; + + el.addEventListener(CIO_EVENTS.carousel.next, handleNav); + el.addEventListener(CIO_EVENTS.carousel.previous, handleNav); + + return () => { + el.removeEventListener(CIO_EVENTS.carousel.next, handleNav); + el.removeEventListener(CIO_EVENTS.carousel.previous, handleNav); + }; + }, []); + + return ; +} +``` + +## Available Events + +Both events carry a `CarouselNavEventDetail` payload with the scroll direction and current scroll state. + +| Event Name | Constant | Fires When | +| ------------------------------------- | --------------------------------- | ---------------------- | +| `cio.components.carousel.next` | `CIO_EVENTS.carousel.next` | Next button clicked | +| `cio.components.carousel.previous` | `CIO_EVENTS.carousel.previous` | Previous button clicked| + +**Payload type:** `CarouselNavEventDetail` + +```ts +interface CarouselNavEventDetail { + direction: 'next' | 'previous'; + canScrollNext: boolean; + canScrollPrev: boolean; +} +``` + +## Notes + +- All events are dispatched with `bubbles: true`, so you can listen on any ancestor element instead of the carousel itself. +- The `detail` payload includes the current scroll state (`canScrollNext`, `canScrollPrev`) so you can update UI accordingly. +- Use `CIO_EVENTS` constants instead of raw strings to avoid typos and get autocomplete. diff --git a/src/stories/components/ProductCard/Code Examples - Events.mdx b/src/stories/components/ProductCard/Code Examples - Events.mdx new file mode 100644 index 0000000..9909aad --- /dev/null +++ b/src/stories/components/ProductCard/Code Examples - Events.mdx @@ -0,0 +1,72 @@ +import { Meta, Canvas } from '@storybook/addon-docs/blocks'; +import * as ProductCardEventsStories from './ProductCardEvents.stories'; + + + +# `ProductCard` - Scoped Event Listening + +Listen for user-interaction events directly on a `ProductCard` instance using a `ref`, instead of on `window`. + +## Why scoped events? + +When multiple `ProductCard` instances exist on the same page, listening on `window` means **every** listener fires for **every** card. Scoped events solve this: each card dispatches events on its own root DOM element, so a listener attached via `ref` only receives events from that specific card. + +Events are dispatched with `bubbles: true`, so you can also listen on any ancestor element if you prefer a single handler for a group of cards. + +## Interactive Demo + +Click the card, hover the image, or click "Add to Cart" to see events appear in the log panel below. + + + +## Basic Setup + +```tsx +import { useRef, useEffect } from 'react'; +import ProductCard from '@constructorio/ui-components/product-card'; +import { CIO_EVENTS } from '@constructorio/ui-components'; + +function MyProductCard({ product }) { + const cardRef = useRef(null); + + useEffect(() => { + const el = cardRef.current; + if (!el) return; + + const handleClick = (e: Event) => { + const { product } = (e as CustomEvent).detail; + console.log('Card clicked:', product.name); + }; + + el.addEventListener(CIO_EVENTS.productCard.click, handleClick); + return () => el.removeEventListener(CIO_EVENTS.productCard.click, handleClick); + }, []); + + return ; +} +``` + +## Available Events + +All events carry a `ProductCardEventDetail` payload containing the `product: Product` for the card that fired. + +| Event Name | Constant | Fires When | +| --------------------------------------------- | ------------------------------------- | ------------------------- | +| `cio.components.productCard.click` | `CIO_EVENTS.productCard.click` | Card is clicked | +| `cio.components.productCard.conversion` | `CIO_EVENTS.productCard.conversion` | Add to Cart is clicked | +| `cio.components.productCard.imageEnter` | `CIO_EVENTS.productCard.imageEnter` | Mouse enters card image | +| `cio.components.productCard.imageLeave` | `CIO_EVENTS.productCard.imageLeave` | Mouse leaves card image | + +**Payload type:** `ProductCardEventDetail` + +```ts +interface ProductCardEventDetail { + product: Product; +} +``` + +## Notes + +- All events are dispatched with `bubbles: true`, so you can listen on any ancestor element instead of the card itself. +- The `detail` payload always contains the `Product` object for the card that fired the event. +- Use `CIO_EVENTS` constants instead of raw strings to avoid typos and get autocomplete. diff --git a/src/stories/components/ProductCard/ProductCardEvents.stories.tsx b/src/stories/components/ProductCard/ProductCardEvents.stories.tsx new file mode 100644 index 0000000..3754e9e --- /dev/null +++ b/src/stories/components/ProductCard/ProductCardEvents.stories.tsx @@ -0,0 +1,92 @@ +import React, { useRef, useEffect, useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import ProductCard from '../../../components/product-card'; +import { CIO_EVENTS } from '../../../utils/events'; +import { DEMO_IMAGE_URL } from '../../constants'; + +const meta = { + title: 'Components/ProductCard', + component: ProductCard, + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +function ProductCardEventListeningDemo() { + const cardRef = useRef(null); + const [eventLog, setEventLog] = useState([]); + + useEffect(() => { + const el = cardRef.current; + if (!el) return; + + const logEvent = (label: string) => (e: Event) => { + const detail = (e as CustomEvent).detail; + setEventLog((prev) => [ + `[${new Date().toLocaleTimeString()}] ${label} — product: ${detail?.product?.name ?? 'N/A'}`, + ...prev.slice(0, 49), + ]); + }; + + const handlers = [ + [CIO_EVENTS.productCard.click, logEvent('productCard.click')] as const, + [CIO_EVENTS.productCard.conversion, logEvent('productCard.conversion')] as const, + [CIO_EVENTS.productCard.imageEnter, logEvent('productCard.imageEnter')] as const, + [CIO_EVENTS.productCard.imageLeave, logEvent('productCard.imageLeave')] as const, + ]; + + handlers.forEach(([name, fn]) => el.addEventListener(name, fn)); + return () => handlers.forEach(([name, fn]) => el.removeEventListener(name, fn)); + }, []); + + return ( +
+ {}} + /> +
+
Event Log
+ {eventLog.length === 0 ? ( +
+ Interact with the card above to see events... +
+ ) : ( + eventLog.map((entry, i) => ( +
+ {entry} +
+ )) + )} +
+
+ ); +} + +export const EventListening: Story = { + render: () => , + tags: ['!autodocs', '!dev'], +}; From 366413827b7617c61683b24ccff4f0586bf62fdf Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 10 Feb 2026 19:54:05 +0400 Subject: [PATCH 5/9] Fix MDX table issue --- .storybook/main.ts | 12 +- package-lock.json | 1333 ++++++++++++++++++++++++++++++++++++++++---- package.json | 1 + 3 files changed, 1241 insertions(+), 105 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 152787e..dbab319 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from '@storybook/react-vite'; +import remarkGfm from 'remark-gfm'; const config: StorybookConfig = { "stories": [ @@ -7,7 +8,16 @@ const config: StorybookConfig = { ], "addons": [ "@chromatic-com/storybook", - "@storybook/addon-docs", + { + name: '@storybook/addon-docs', + options: { + mdxPluginOptions: { + mdxCompileOptions: { + remarkPlugins: [remarkGfm], + }, + }, + }, + }, "@storybook/addon-a11y", "@storybook/addon-vitest" ], diff --git a/package-lock.json b/package-lock.json index 4445b01..e239f80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "lint-staged": "^16.1.5", "playwright": "^1.56.1", "prettier": "3.6.2", + "remark-gfm": "^4.0.1", "start-server-and-test": "^2.0.13", "storybook": "^9.1.17", "tsc-alias": "^1.8.16", @@ -4983,6 +4984,16 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -5017,6 +5028,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -5024,6 +5045,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.3.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", @@ -5061,6 +5089,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.40.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", @@ -6267,6 +6302,17 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -6465,6 +6511,17 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -6499,6 +6556,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -7060,6 +7128,20 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -7152,6 +7234,20 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -8167,6 +8263,13 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9356,6 +9459,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -10335,6 +10451,17 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10399,6 +10526,17 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10409,171 +10547,975 @@ "node": ">= 0.4" } }, - "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, "engines": { - "node": ">=8.6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mimic-fn": { + "node_modules/mdast-util-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "dev": true, "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dev": true, "license": "MIT", "dependencies": { - "minipass": "^7.1.2" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">= 18" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.6" + "@types/mdast": "^4.0.0" }, - "bin": { - "mkdirp": "bin/cmd.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mri": { + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", @@ -11803,6 +12745,58 @@ "node": ">=6" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -13110,6 +14104,17 @@ "node": ">=0.6" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -13419,6 +14424,85 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -13537,6 +14621,36 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", @@ -14136,6 +15250,17 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 46a67eb..8642fb5 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "lint-staged": "^16.1.5", "playwright": "^1.56.1", "prettier": "3.6.2", + "remark-gfm": "^4.0.1", "start-server-and-test": "^2.0.13", "storybook": "^9.1.17", "tsc-alias": "^1.8.16", From f21b4f4cf533742b3f293e2a137ce456da22d097 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Tue, 10 Feb 2026 19:55:20 +0400 Subject: [PATCH 6/9] Lint --- src/stories/components/Carousel/CarouselEvents.stories.tsx | 4 +--- .../components/ProductCard/ProductCardEvents.stories.tsx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/stories/components/Carousel/CarouselEvents.stories.tsx b/src/stories/components/Carousel/CarouselEvents.stories.tsx index 8bec9ab..bb46ef3 100644 --- a/src/stories/components/Carousel/CarouselEvents.stories.tsx +++ b/src/stories/components/Carousel/CarouselEvents.stories.tsx @@ -69,9 +69,7 @@ function CarouselEventListeningDemo() { }}>
Event Log
{eventLog.length === 0 ? ( -
- Click the carousel arrows to see events... -
+
Click the carousel arrows to see events...
) : ( eventLog.map((entry, i) => (
diff --git a/src/stories/components/ProductCard/ProductCardEvents.stories.tsx b/src/stories/components/ProductCard/ProductCardEvents.stories.tsx index 3754e9e..5e0e714 100644 --- a/src/stories/components/ProductCard/ProductCardEvents.stories.tsx +++ b/src/stories/components/ProductCard/ProductCardEvents.stories.tsx @@ -71,9 +71,7 @@ function ProductCardEventListeningDemo() { }}>
Event Log
{eventLog.length === 0 ? ( -
- Interact with the card above to see events... -
+
Interact with the card above to see events...
) : ( eventLog.map((entry, i) => (
From 7017c18208ee047e9000c4a9537777967d497799 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Thu, 26 Feb 2026 16:12:47 +0300 Subject: [PATCH 7/9] Remove rootRef --- src/components/carousel.tsx | 54 +++++++++--------------- src/components/product-card.tsx | 75 +++++++++++++++------------------ src/types/carouselTypes.ts | 3 +- 3 files changed, 55 insertions(+), 77 deletions(-) diff --git a/src/components/carousel.tsx b/src/components/carousel.tsx index 9af1cb2..07c57b1 100644 --- a/src/components/carousel.tsx +++ b/src/components/carousel.tsx @@ -5,7 +5,6 @@ import React, { useState, useCallback, useEffect, - useRef, forwardRef, } from 'react'; import useEmblaCarousel from 'embla-carousel-react'; @@ -86,7 +85,6 @@ function CarouselBase({ }, plugins, ); - const rootRef = useRef(null); const { rootProps } = useCarouselResponsive(responsive, orientation); useCarouselTweenOpacity(api, orientation); @@ -152,7 +150,6 @@ function CarouselBase({ }, componentOverrides: componentOverrides as CarouselOverrides | undefined, carouselRef, - rootRef, }; }, [ orientation, @@ -169,24 +166,10 @@ function CarouselBase({ carouselRef, ]); - const setRootRef = useCallback( - (node: HTMLDivElement | null) => { - // Set internal rootRef - (rootRef as React.MutableRefObject).current = node; - // Forward ref to consumer - if (typeof innerRef === 'function') { - innerRef(node); - } else if (innerRef) { - (innerRef as React.MutableRefObject).current = node; - } - }, - [innerRef], - ); - return (
{ - const eventName = isPrevious ? CIO_EVENTS.carousel.previous : CIO_EVENTS.carousel.next; - - dispatchCioEvent( - eventName, - { - direction, - canScrollNext: canScrollNext ?? false, - canScrollPrev: canScrollPrev ?? false, - }, - rootRef.current, - ); - - scrollFn?.(); - }, [isPrevious, direction, canScrollNext, canScrollPrev, scrollFn, rootRef]); + const handleClick = useCallback( + (e: React.MouseEvent) => { + const eventName = isPrevious ? CIO_EVENTS.carousel.previous : CIO_EVENTS.carousel.next; + + dispatchCioEvent( + eventName, + { + direction, + canScrollNext: canScrollNext ?? false, + canScrollPrev: canScrollPrev ?? false, + }, + e.currentTarget, + ); + + scrollFn?.(); + }, + [isPrevious, direction, canScrollNext, canScrollPrev, scrollFn], + ); const override = isPrevious ? componentOverrides?.previous?.reactNode diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx index b08144e..ebe9f5a 100644 --- a/src/components/product-card.tsx +++ b/src/components/product-card.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useCallback, useContext, useRef, forwardRef } from 'react'; +import React, { createContext, useCallback, useContext, forwardRef } from 'react'; import { cn, RenderPropsWrapper, dispatchCioEvent, CIO_EVENTS } from '@/utils'; import { Card, CardContentProps, CardFooterProps } from '@/components/card'; import Button from '@/components/button'; @@ -25,7 +25,6 @@ import { interface ProductCardContextValue { renderProps: Omit; componentOverrides?: ProductCardOverrides; - rootRef: React.RefObject; } const ProductCardContext = createContext(null); @@ -155,27 +154,33 @@ const TagsSection: React.FC = (props) => { }; const ImageSection: React.FC = (props) => { - const { renderProps, componentOverrides, rootRef } = useProductCardContext(); + const { renderProps, componentOverrides } = useProductCardContext(); const { imageUrl: contextImageUrl, name } = renderProps.product; // Use props with fallback to context values const imageUrl = props.imageUrl || contextImageUrl; - const handleMouseEnter = useCallback(() => { - dispatchCioEvent( - CIO_EVENTS.productCard.imageEnter, - { product: renderProps.product }, - rootRef.current, - ); - }, [renderProps.product, rootRef]); - - const handleMouseLeave = useCallback(() => { - dispatchCioEvent( - CIO_EVENTS.productCard.imageLeave, - { product: renderProps.product }, - rootRef.current, - ); - }, [renderProps.product, rootRef]); + const handleMouseEnter = useCallback( + (e: React.MouseEvent) => { + dispatchCioEvent( + CIO_EVENTS.productCard.imageEnter, + { product: renderProps.product }, + e.currentTarget, + ); + }, + [renderProps.product], + ); + + const handleMouseLeave = useCallback( + (e: React.MouseEvent) => { + dispatchCioEvent( + CIO_EVENTS.productCard.imageLeave, + { product: renderProps.product }, + e.currentTarget, + ); + }, + [renderProps.product], + ); return ( @@ -241,7 +246,7 @@ const DescriptionSection: React.FC = (props) => { }; const AddToCartButton: React.FC = (props) => { - const { renderProps, componentOverrides, rootRef } = useProductCardContext(); + const { renderProps, componentOverrides } = useProductCardContext(); const { addToCartText = renderProps.addToCartText || 'Add to Cart', onAddToCart = renderProps.onAddToCart, @@ -255,11 +260,11 @@ const AddToCartButton: React.FC = (props) => { dispatchCioEvent( CIO_EVENTS.productCard.conversion, { product: renderProps.product }, - rootRef.current, + e.currentTarget, ); onAddToCart?.(e); }, - [renderProps.product, onAddToCart, rootRef], + [renderProps.product, onAddToCart], ); return ( @@ -369,25 +374,10 @@ const ProductCard = forwardRef(function Produc { componentOverrides, children, className, ...props }, ref, ) { - const rootRef = useRef(null); - - const setRootRef = useCallback( - (node: HTMLDivElement | null) => { - (rootRef as React.MutableRefObject).current = node; - if (typeof ref === 'function') { - ref(node); - } else if (ref) { - (ref as React.MutableRefObject).current = node; - } - }, - [ref], - ); - const contextValue = React.useMemo( () => ({ renderProps: { ...props, ...getProductCardDataAttributes(props.product) }, componentOverrides, - rootRef, }), [props, componentOverrides], ); @@ -404,10 +394,13 @@ const ProductCard = forwardRef(function Produc ...restProps } = props; - const handleProductClick = useCallback(() => { - dispatchCioEvent(CIO_EVENTS.productCard.click, { product }, rootRef.current); - onProductClick?.(); - }, [product, onProductClick]); + const handleProductClick = useCallback( + (e: React.MouseEvent) => { + dispatchCioEvent(CIO_EVENTS.productCard.click, { product }, e.currentTarget); + onProductClick?.(); + }, + [product, onProductClick], + ); const renderPropFn = typeof children === 'function' && children; @@ -416,7 +409,7 @@ const ProductCard = forwardRef(function Produc = { export type CarouselContextValue = { carouselRef: ReturnType[0]; - rootRef: RefObject; renderProps: CarouselRenderProps; componentOverrides?: CarouselOverrides; } & CarouselProps; From 80f97121ba710a94904d005938b3984afd6608bb Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Thu, 26 Feb 2026 22:24:01 +0300 Subject: [PATCH 8/9] Remove refs --- spec/components/Carousel/Carousel.test.tsx | 37 ++++---- .../product-card/product-card.test.tsx | 90 ++++++++++--------- src/components/card.tsx | 35 +++----- src/components/carousel.tsx | 32 ++----- src/components/product-card.tsx | 53 ++++------- .../Carousel/CarouselEvents.stories.tsx | 10 ++- .../Carousel/Code Examples - Events.mdx | 16 ++-- .../ProductCard/Code Examples - Events.mdx | 16 ++-- .../ProductCard/ProductCardEvents.stories.tsx | 9 +- 9 files changed, 131 insertions(+), 167 deletions(-) diff --git a/spec/components/Carousel/Carousel.test.tsx b/spec/components/Carousel/Carousel.test.tsx index 0011d29..b7bbeb0 100644 --- a/spec/components/Carousel/Carousel.test.tsx +++ b/spec/components/Carousel/Carousel.test.tsx @@ -604,11 +604,11 @@ describe('Carousel component', () => { }); test('dispatches carousel.next event on root element when next button is clicked', () => { - const ref = React.createRef(); - render(); + const { container } = render(); + const el = container.querySelector('[data-slot="carousel"]')!; const listener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.carousel.next, listener); + el.addEventListener(CIO_EVENTS.carousel.next, listener); const nextButton = screen.getByRole('button', { name: /next/i }); fireEvent.click(nextButton); @@ -619,15 +619,15 @@ describe('Carousel component', () => { expect(typeof event.detail.canScrollNext).toBe('boolean'); expect(typeof event.detail.canScrollPrev).toBe('boolean'); - ref.current!.removeEventListener(CIO_EVENTS.carousel.next, listener); + el.removeEventListener(CIO_EVENTS.carousel.next, listener); }); test('dispatches carousel.previous event on root element when previous button is clicked', () => { - const ref = React.createRef(); - render(); + const { container } = render(); + const el = container.querySelector('[data-slot="carousel"]')!; const listener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.carousel.previous, listener); + el.addEventListener(CIO_EVENTS.carousel.previous, listener); const prevButton = screen.getByRole('button', { name: /previous/i }); fireEvent.click(prevButton); @@ -638,7 +638,7 @@ describe('Carousel component', () => { expect(typeof event.detail.canScrollNext).toBe('boolean'); expect(typeof event.detail.canScrollPrev).toBe('boolean'); - ref.current!.removeEventListener(CIO_EVENTS.carousel.previous, listener); + el.removeEventListener(CIO_EVENTS.carousel.previous, listener); }); test('events bubble up so window listeners still work', () => { @@ -669,20 +669,23 @@ describe('Carousel component', () => { }); test('two carousels: events do not cross-pollinate', () => { - const ref1 = React.createRef(); - const ref2 = React.createRef(); - render( <> - - +
+ +
+
+ +
, ); + const wrapper1 = screen.getByTestId('wrapper-1'); + const wrapper2 = screen.getByTestId('wrapper-2'); const listener1 = vi.fn(); const listener2 = vi.fn(); - ref1.current!.addEventListener(CIO_EVENTS.carousel.next, listener1); - ref2.current!.addEventListener(CIO_EVENTS.carousel.next, listener2); + wrapper1.addEventListener(CIO_EVENTS.carousel.next, listener1); + wrapper2.addEventListener(CIO_EVENTS.carousel.next, listener2); // Click next on the first carousel only const nextButtons = screen.getAllByRole('button', { name: /next/i }); @@ -691,8 +694,8 @@ describe('Carousel component', () => { expect(listener1).toHaveBeenCalledTimes(1); expect(listener2).not.toHaveBeenCalled(); - ref1.current!.removeEventListener(CIO_EVENTS.carousel.next, listener1); - ref2.current!.removeEventListener(CIO_EVENTS.carousel.next, listener2); + wrapper1.removeEventListener(CIO_EVENTS.carousel.next, listener1); + wrapper2.removeEventListener(CIO_EVENTS.carousel.next, listener2); }); }); }); diff --git a/spec/components/product-card/product-card.test.tsx b/spec/components/product-card/product-card.test.tsx index ecf5b55..ea054c3 100644 --- a/spec/components/product-card/product-card.test.tsx +++ b/spec/components/product-card/product-card.test.tsx @@ -401,42 +401,41 @@ describe('ProductCard component', () => { }); test('dispatches productCard.click event on root element with correct product detail', () => { - const ref = React.createRef(); - render(); + render(); + const el = screen.getByTestId('product-card'); const listener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.productCard.click, listener); + el.addEventListener(CIO_EVENTS.productCard.click, listener); - fireEvent.click(screen.getByTestId('product-card')); + fireEvent.click(el); expect(listener).toHaveBeenCalledTimes(1); const event = listener.mock.calls[0][0] as CustomEvent; expect(event.detail.product).toEqual(mockProductData.product); - ref.current!.removeEventListener(CIO_EVENTS.productCard.click, listener); + el.removeEventListener(CIO_EVENTS.productCard.click, listener); }); test('dispatches productCard.click event AND calls onProductClick callback', () => { - const ref = React.createRef(); const mockOnProductClick = vi.fn(); render( , ); + const el = screen.getByTestId('product-card'); const listener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.productCard.click, listener); + el.addEventListener(CIO_EVENTS.productCard.click, listener); - fireEvent.click(screen.getByTestId('product-card')); + fireEvent.click(el); expect(listener).toHaveBeenCalledTimes(1); expect(mockOnProductClick).toHaveBeenCalledTimes(1); - ref.current!.removeEventListener(CIO_EVENTS.productCard.click, listener); + el.removeEventListener(CIO_EVENTS.productCard.click, listener); }); test('events bubble up so window listeners still work', () => { @@ -452,26 +451,26 @@ describe('ProductCard component', () => { }); test('dispatches productCard.click event even without onProductClick prop', () => { - const ref = React.createRef(); - render(); + render(); + const el = screen.getByTestId('product-card'); const listener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.productCard.click, listener); + el.addEventListener(CIO_EVENTS.productCard.click, listener); - fireEvent.click(screen.getByTestId('product-card')); + fireEvent.click(el); expect(listener).toHaveBeenCalledTimes(1); - ref.current!.removeEventListener(CIO_EVENTS.productCard.click, listener); + el.removeEventListener(CIO_EVENTS.productCard.click, listener); }); test('dispatches productCard.conversion event on root element on add-to-cart click', () => { - const ref = React.createRef(); const mockOnAddToCart = vi.fn(); - render(); + render(); + const el = screen.getByTestId('product-card'); const listener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.productCard.conversion, listener); + el.addEventListener(CIO_EVENTS.productCard.conversion, listener); fireEvent.click(screen.getByText('Add to Cart')); @@ -480,78 +479,81 @@ describe('ProductCard component', () => { expect(event.detail.product).toEqual(mockProductData.product); expect(mockOnAddToCart).toHaveBeenCalledTimes(1); - ref.current!.removeEventListener(CIO_EVENTS.productCard.conversion, listener); + el.removeEventListener(CIO_EVENTS.productCard.conversion, listener); }); test('clicking add-to-cart does NOT also dispatch productCard.click', () => { - const ref = React.createRef(); - render(); + render(); + const el = screen.getByTestId('product-card'); const clickListener = vi.fn(); const conversionListener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.productCard.click, clickListener); - ref.current!.addEventListener(CIO_EVENTS.productCard.conversion, conversionListener); + el.addEventListener(CIO_EVENTS.productCard.click, clickListener); + el.addEventListener(CIO_EVENTS.productCard.conversion, conversionListener); fireEvent.click(screen.getByText('Add to Cart')); expect(conversionListener).toHaveBeenCalledTimes(1); expect(clickListener).not.toHaveBeenCalled(); - ref.current!.removeEventListener(CIO_EVENTS.productCard.click, clickListener); - ref.current!.removeEventListener(CIO_EVENTS.productCard.conversion, conversionListener); + el.removeEventListener(CIO_EVENTS.productCard.click, clickListener); + el.removeEventListener(CIO_EVENTS.productCard.conversion, conversionListener); }); test('dispatches productCard.imageEnter on root element on mouseEnter of image section', () => { - const ref = React.createRef(); - render(); + render(); + const el = screen.getByTestId('product-card'); const listener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.productCard.imageEnter, listener); + el.addEventListener(CIO_EVENTS.productCard.imageEnter, listener); - const imageSection = ref.current!.querySelector('.cio-product-card-image-section')!; + const imageSection = el.querySelector('.cio-product-card-image-section')!; fireEvent.mouseEnter(imageSection); expect(listener).toHaveBeenCalledTimes(1); const event = listener.mock.calls[0][0] as CustomEvent; expect(event.detail.product).toEqual(mockProductData.product); - ref.current!.removeEventListener(CIO_EVENTS.productCard.imageEnter, listener); + el.removeEventListener(CIO_EVENTS.productCard.imageEnter, listener); }); test('dispatches productCard.imageLeave on root element on mouseLeave of image section', () => { - const ref = React.createRef(); - render(); + render(); + const el = screen.getByTestId('product-card'); const listener = vi.fn(); - ref.current!.addEventListener(CIO_EVENTS.productCard.imageLeave, listener); + el.addEventListener(CIO_EVENTS.productCard.imageLeave, listener); - const imageSection = ref.current!.querySelector('.cio-product-card-image-section')!; + const imageSection = el.querySelector('.cio-product-card-image-section')!; fireEvent.mouseLeave(imageSection); expect(listener).toHaveBeenCalledTimes(1); const event = listener.mock.calls[0][0] as CustomEvent; expect(event.detail.product).toEqual(mockProductData.product); - ref.current!.removeEventListener(CIO_EVENTS.productCard.imageLeave, listener); + el.removeEventListener(CIO_EVENTS.productCard.imageLeave, listener); }); test('two product cards: events do not cross-pollinate', () => { - const ref1 = React.createRef(); - const ref2 = React.createRef(); - const product2 = { ...mockProductData, product: { ...mockProductData.product, id: 'product-2', name: 'Product 2' } }; render( <> - - +
+ +
+
+ +
, ); + const wrapper1 = screen.getByTestId('wrapper-1'); + const wrapper2 = screen.getByTestId('wrapper-2'); const listener1 = vi.fn(); const listener2 = vi.fn(); - ref1.current!.addEventListener(CIO_EVENTS.productCard.click, listener1); - ref2.current!.addEventListener(CIO_EVENTS.productCard.click, listener2); + wrapper1.addEventListener(CIO_EVENTS.productCard.click, listener1); + wrapper2.addEventListener(CIO_EVENTS.productCard.click, listener2); // Click only the first card fireEvent.click(screen.getByTestId('card-1')); @@ -559,8 +561,8 @@ describe('ProductCard component', () => { expect(listener1).toHaveBeenCalledTimes(1); expect(listener2).not.toHaveBeenCalled(); - ref1.current!.removeEventListener(CIO_EVENTS.productCard.click, listener1); - ref2.current!.removeEventListener(CIO_EVENTS.productCard.click, listener2); + wrapper1.removeEventListener(CIO_EVENTS.productCard.click, listener1); + wrapper2.removeEventListener(CIO_EVENTS.productCard.click, listener2); }); }); }); diff --git a/src/components/card.tsx b/src/components/card.tsx index 086fab3..af74ff8 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, createContext, useContext, forwardRef } from 'react'; +import React, { ReactNode, createContext, useContext } from 'react'; import { cn, RenderPropsWrapper } from '@/utils'; import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; @@ -64,12 +64,7 @@ const useCardContext = (): CardContextType => { return context; }; -// forwardRef so parent components (e.g. ProductCard) can attach a ref to the root div -// for dispatching scoped custom events on this element instead of window -const Card = forwardRef(function Card( - { children, componentOverrides, className, ...props }, - ref, -) { +function Card({ children, componentOverrides, className, ...props }: CardProps) { const contextValue: CardContextType = React.useMemo( () => ({ renderProps: props, // Use merged props in context @@ -82,7 +77,6 @@ const Card = forwardRef(function Card(
(function Card( ); -}); +} function CardHeader({ children, className, ...props }: CardHeaderProps) { const { renderProps, componentOverrides } = useCardContext(); @@ -186,21 +180,14 @@ function CardFooter({ children, className, ...props }: CardFooterProps) { ); } -// Attach compound sub-components to the forwardRef'd Card -const CardNamespace = Card as typeof Card & { - Header: typeof CardHeader; - Title: typeof CardTitle; - Description: typeof CardDescription; - Action: typeof CardAction; - Content: typeof CardContent; - Footer: typeof CardFooter; -}; -CardNamespace.Header = CardHeader; -CardNamespace.Title = CardTitle; -CardNamespace.Description = CardDescription; -CardNamespace.Action = CardAction; -CardNamespace.Content = CardContent; -CardNamespace.Footer = CardFooter; +const CardNamespace = Object.assign(Card, { + Header: CardHeader, + Title: CardTitle, + Description: CardDescription, + Action: CardAction, + Content: CardContent, + Footer: CardFooter, +}); export { CardNamespace as Card, diff --git a/src/components/carousel.tsx b/src/components/carousel.tsx index 07c57b1..99ec2fb 100644 --- a/src/components/carousel.tsx +++ b/src/components/carousel.tsx @@ -5,7 +5,6 @@ import React, { useState, useCallback, useEffect, - forwardRef, } from 'react'; import useEmblaCarousel from 'embla-carousel-react'; import Autoplay from 'embla-carousel-autoplay'; @@ -62,9 +61,8 @@ function CarouselBase({ children, componentOverrides, plugins, - innerRef, ...props -}: ComponentProps<'div'> & CarouselProps & { innerRef?: React.Ref }) { +}: ComponentProps<'div'> & CarouselProps) { const { orientation = 'horizontal', autoPlay, @@ -169,7 +167,6 @@ function CarouselBase({ return (
({ ); } -function CarouselInner(props: CarouselOpts, ref: React.Ref) { +function Carousel(props: CarouselOpts) { const { children, items, componentOverrides, ...rest } = props; const { autoPlay, slidesToScroll, orientation, loop, responsive } = rest; @@ -200,7 +197,6 @@ function CarouselInner(props: CarouselOpts, ref: React.Ref(props: CarouselOpts, ref: React.Ref( - props: CarouselOpts & { ref?: React.Ref }, -) => React.ReactElement | null; - function CarouselContent({ className, children, ...props }: ComponentProps<'div'>) { const { carouselRef, renderProps, componentOverrides } = useCarousel(); const { orientation } = renderProps; @@ -378,16 +367,11 @@ function CarouselNext(props: NavButtonProps) { return ; } -// Attach compound sub-components to the forwardRef'd Carousel -const CarouselNamespace = Carousel as typeof Carousel & { - Content: typeof CarouselContent; - Item: typeof CarouselItem; - Previous: typeof CarouselPrevious; - Next: typeof CarouselNext; -}; -CarouselNamespace.Content = CarouselContent; -CarouselNamespace.Item = CarouselItem; -CarouselNamespace.Previous = CarouselPrevious; -CarouselNamespace.Next = CarouselNext; +const CarouselNamespace = Object.assign(Carousel, { + Content: CarouselContent, + Item: CarouselItem, + Previous: CarouselPrevious, + Next: CarouselNext, +}); export default CarouselNamespace; diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx index ebe9f5a..b3107d1 100644 --- a/src/components/product-card.tsx +++ b/src/components/product-card.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useCallback, useContext, forwardRef } from 'react'; +import React, { createContext, useCallback, useContext } from 'react'; import { cn, RenderPropsWrapper, dispatchCioEvent, CIO_EVENTS } from '@/utils'; import { Card, CardContentProps, CardFooterProps } from '@/components/card'; import Button from '@/components/button'; @@ -368,12 +368,7 @@ function getProductCardDataAttributes({ }; } -// forwardRef so consumers can access the root element to listen for scoped custom events -// (e.g. ref.current.addEventListener(CIO_EVENTS.productCard.click, handler)) -const ProductCard = forwardRef(function ProductCard( - { componentOverrides, children, className, ...props }, - ref, -) { +function ProductCard({ componentOverrides, children, className, ...props }: ProductCardProps) { const contextValue = React.useMemo( () => ({ renderProps: { ...props, ...getProductCardDataAttributes(props.product) }, @@ -409,7 +404,6 @@ const ProductCard = forwardRef(function Produc (function Produc ); -}); +} -// Attach compound sub-components to the forwardRef'd ProductCard -const ProductCardNamespace = ProductCard as typeof ProductCard & { - ImageSection: typeof ImageSection; - Badge: typeof Badge; - WishlistButton: typeof WishlistButton; - PriceSection: typeof PriceSection; - TitleSection: typeof TitleSection; - DescriptionSection: typeof DescriptionSection; - RatingSection: typeof RatingSection; - TagsSection: typeof TagsSection; - AddToCartButton: typeof AddToCartButton; - Content: typeof ProductCardContent; - Footer: typeof ProductCardFooter; - getProductCardDataAttributes: typeof getProductCardDataAttributes; -}; -ProductCardNamespace.ImageSection = ImageSection; -ProductCardNamespace.Badge = Badge; -ProductCardNamespace.WishlistButton = WishlistButton; -ProductCardNamespace.PriceSection = PriceSection; -ProductCardNamespace.TitleSection = TitleSection; -ProductCardNamespace.DescriptionSection = DescriptionSection; -ProductCardNamespace.RatingSection = RatingSection; -ProductCardNamespace.TagsSection = TagsSection; -ProductCardNamespace.AddToCartButton = AddToCartButton; -ProductCardNamespace.Content = ProductCardContent; -ProductCardNamespace.Footer = ProductCardFooter; -ProductCardNamespace.getProductCardDataAttributes = getProductCardDataAttributes; +const ProductCardNamespace = Object.assign(ProductCard, { + ImageSection, + Badge, + WishlistButton, + PriceSection, + TitleSection, + DescriptionSection, + RatingSection, + TagsSection, + AddToCartButton, + Content: ProductCardContent, + Footer: ProductCardFooter, + getProductCardDataAttributes, +}); export default ProductCardNamespace; diff --git a/src/stories/components/Carousel/CarouselEvents.stories.tsx b/src/stories/components/Carousel/CarouselEvents.stories.tsx index bb46ef3..6f0a92a 100644 --- a/src/stories/components/Carousel/CarouselEvents.stories.tsx +++ b/src/stories/components/Carousel/CarouselEvents.stories.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import CioCarousel from '../../../components/carousel'; import { Product } from '../../../types/productCardTypes'; @@ -29,11 +29,11 @@ const mockProducts: Product[] = Array.from({ length: 10 }, (_, i) => ({ })); function CarouselEventListeningDemo() { - const carouselRef = useRef(null); + const wrapperRef = useRef(null); const [eventLog, setEventLog] = useState([]); useEffect(() => { - const el = carouselRef.current; + const el = wrapperRef.current; if (!el) return; const logEvent = (label: string) => (e: Event) => { @@ -55,7 +55,9 @@ function CarouselEventListeningDemo() { return (
- +
+ +
(null); + const wrapperRef = useRef(null); useEffect(() => { - const el = carouselRef.current; + const el = wrapperRef.current; if (!el) return; const handleNav = (e: Event) => { @@ -47,7 +45,11 @@ function MyCarousel({ products }) { }; }, []); - return ; + return ( +
+ +
+ ); } ``` diff --git a/src/stories/components/ProductCard/Code Examples - Events.mdx b/src/stories/components/ProductCard/Code Examples - Events.mdx index 9909aad..3a04c45 100644 --- a/src/stories/components/ProductCard/Code Examples - Events.mdx +++ b/src/stories/components/ProductCard/Code Examples - Events.mdx @@ -5,13 +5,11 @@ import * as ProductCardEventsStories from './ProductCardEvents.stories'; # `ProductCard` - Scoped Event Listening -Listen for user-interaction events directly on a `ProductCard` instance using a `ref`, instead of on `window`. +Listen for user-interaction events on a `ProductCard` instance by wrapping it in a container element, instead of listening on `window`. ## Why scoped events? -When multiple `ProductCard` instances exist on the same page, listening on `window` means **every** listener fires for **every** card. Scoped events solve this: each card dispatches events on its own root DOM element, so a listener attached via `ref` only receives events from that specific card. - -Events are dispatched with `bubbles: true`, so you can also listen on any ancestor element if you prefer a single handler for a group of cards. +When multiple `ProductCard` instances exist on the same page, listening on `window` means **every** listener fires for **every** card. Scoped events solve this: each card dispatches events on its own root DOM element with `bubbles: true`, so a listener attached to any ancestor element only receives events from cards within that subtree. ## Interactive Demo @@ -27,10 +25,10 @@ import ProductCard from '@constructorio/ui-components/product-card'; import { CIO_EVENTS } from '@constructorio/ui-components'; function MyProductCard({ product }) { - const cardRef = useRef(null); + const wrapperRef = useRef(null); useEffect(() => { - const el = cardRef.current; + const el = wrapperRef.current; if (!el) return; const handleClick = (e: Event) => { @@ -42,7 +40,11 @@ function MyProductCard({ product }) { return () => el.removeEventListener(CIO_EVENTS.productCard.click, handleClick); }, []); - return ; + return ( +
+ +
+ ); } ``` diff --git a/src/stories/components/ProductCard/ProductCardEvents.stories.tsx b/src/stories/components/ProductCard/ProductCardEvents.stories.tsx index 5e0e714..12f1cf0 100644 --- a/src/stories/components/ProductCard/ProductCardEvents.stories.tsx +++ b/src/stories/components/ProductCard/ProductCardEvents.stories.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import ProductCard from '../../../components/product-card'; import { CIO_EVENTS } from '../../../utils/events'; @@ -16,11 +16,11 @@ export default meta; type Story = StoryObj; function ProductCardEventListeningDemo() { - const cardRef = useRef(null); + const wrapperRef = useRef(null); const [eventLog, setEventLog] = useState([]); useEffect(() => { - const el = cardRef.current; + const el = wrapperRef.current; if (!el) return; const logEvent = (label: string) => (e: Event) => { @@ -44,8 +44,8 @@ function ProductCardEventListeningDemo() { return (
+
{}} /> +
Date: Thu, 26 Feb 2026 22:25:16 +0300 Subject: [PATCH 9/9] Revert export style --- src/components/card.tsx | 27 +++++++++------------------ src/components/carousel.tsx | 15 +++++++-------- src/components/product-card.tsx | 31 +++++++++++++++---------------- 3 files changed, 31 insertions(+), 42 deletions(-) diff --git a/src/components/card.tsx b/src/components/card.tsx index af74ff8..59a5691 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -180,21 +180,12 @@ function CardFooter({ children, className, ...props }: CardFooterProps) { ); } -const CardNamespace = Object.assign(Card, { - Header: CardHeader, - Title: CardTitle, - Description: CardDescription, - Action: CardAction, - Content: CardContent, - Footer: CardFooter, -}); - -export { - CardNamespace as Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -}; +// Attach compound components to Card +Card.Header = CardHeader; +Card.Title = CardTitle; +Card.Description = CardDescription; +Card.Action = CardAction; +Card.Content = CardContent; +Card.Footer = CardFooter; + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/src/components/carousel.tsx b/src/components/carousel.tsx index 99ec2fb..03674f0 100644 --- a/src/components/carousel.tsx +++ b/src/components/carousel.tsx @@ -367,11 +367,10 @@ function CarouselNext(props: NavButtonProps) { return ; } -const CarouselNamespace = Object.assign(Carousel, { - Content: CarouselContent, - Item: CarouselItem, - Previous: CarouselPrevious, - Next: CarouselNext, -}); - -export default CarouselNamespace; +// Attach compound components to Carousel +Carousel.Content = CarouselContent; +Carousel.Item = CarouselItem; +Carousel.Previous = CarouselPrevious; +Carousel.Next = CarouselNext; + +export default Carousel; diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx index b3107d1..ea6c037 100644 --- a/src/components/product-card.tsx +++ b/src/components/product-card.tsx @@ -440,19 +440,18 @@ function ProductCard({ componentOverrides, children, className, ...props }: Prod ); } -const ProductCardNamespace = Object.assign(ProductCard, { - ImageSection, - Badge, - WishlistButton, - PriceSection, - TitleSection, - DescriptionSection, - RatingSection, - TagsSection, - AddToCartButton, - Content: ProductCardContent, - Footer: ProductCardFooter, - getProductCardDataAttributes, -}); - -export default ProductCardNamespace; +// Attach compound components to ProductCard +ProductCard.ImageSection = ImageSection; +ProductCard.Badge = Badge; +ProductCard.WishlistButton = WishlistButton; +ProductCard.PriceSection = PriceSection; +ProductCard.TitleSection = TitleSection; +ProductCard.DescriptionSection = DescriptionSection; +ProductCard.RatingSection = RatingSection; +ProductCard.TagsSection = TagsSection; +ProductCard.AddToCartButton = AddToCartButton; +ProductCard.Content = ProductCardContent; +ProductCard.Footer = ProductCardFooter; +ProductCard.getProductCardDataAttributes = getProductCardDataAttributes; + +export default ProductCard;