diff --git a/components/Avatar/Avatar.stories.tsx b/components/Avatar/Avatar.stories.tsx index edd5e673..bab01d9c 100644 --- a/components/Avatar/Avatar.stories.tsx +++ b/components/Avatar/Avatar.stories.tsx @@ -2,7 +2,11 @@ import { Meta, StoryFn } from '@storybook/react-vite'; import React from 'react'; +import { BoxVanilla } from '../Box/Box.vanilla'; +import { FlexVanilla } from '../Flex/Flex.vanilla'; +import { H3Vanilla } from '../Heading/Heading.vanilla'; import { Avatar } from './Avatar'; +import { AvatarVanilla } from './Avatar.vanilla'; const Component: Meta = { title: 'Components/Avatar', @@ -93,4 +97,59 @@ Variants.argTypes = { }, }; +export const Comparison: StoryFn = () => ( + + + Stitches Version + + + + + + + + + Fallback Variants + + + + + + + + + Shapes + + + + + + + Vanilla Extract Version + + + + + + + + + Fallback Variants + + + + + + + + + Shapes + + + + + + +); + export default Component; diff --git a/components/Avatar/Avatar.vanilla.css.ts b/components/Avatar/Avatar.vanilla.css.ts new file mode 100644 index 00000000..b448a7db --- /dev/null +++ b/components/Avatar/Avatar.vanilla.css.ts @@ -0,0 +1,151 @@ +import { style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; + +import { tokens } from '../../styles/tokens.css'; + +const avatarRoot = style({ + alignItems: 'center', + justifyContent: 'center', + verticalAlign: 'middle', + overflow: 'hidden', + userSelect: 'none', + boxSizing: 'border-box', + display: 'flex', + flexShrink: 0, + position: 'relative', + border: 'none', + fontFamily: 'inherit', + lineHeight: '1', + margin: '0', + outline: 'none', + padding: '0', + fontWeight: tokens.fontWeights.medium, + color: tokens.colors.hiContrast, + + selectors: { + '&::before': { + content: '""', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + borderRadius: 'inherit', + boxShadow: 'inset 0px 0px 1px rgba(0, 0, 0, 0.12)', + }, + }, +}); + +export const avatarRecipe = recipe({ + base: avatarRoot, + + variants: { + size: { + '1': { + width: tokens.sizes['3'], + height: tokens.sizes['3'], + }, + '2': { + width: tokens.sizes['5'], + height: tokens.sizes['5'], + }, + '3': { + width: tokens.sizes['6'], + height: tokens.sizes['6'], + }, + '4': { + width: tokens.sizes['7'], + height: tokens.sizes['7'], + }, + '5': { + width: tokens.sizes['8'], + height: tokens.sizes['8'], + }, + '6': { + width: tokens.sizes['9'], + height: tokens.sizes['9'], + }, + }, + variant: { + gray: { + backgroundColor: tokens.colors.slate['5'], + }, + red: { + backgroundColor: tokens.colors.red['5'], + }, + purple: { + backgroundColor: tokens.colors.purple['5'], + }, + blue: { + backgroundColor: tokens.colors.blue['5'], + }, + green: { + backgroundColor: tokens.colors.green['5'], + }, + orange: { + backgroundColor: tokens.colors.orange['5'], + }, + }, + shape: { + square: { + borderRadius: tokens.radii['2'], + }, + circle: { + borderRadius: '50%', + }, + }, + }, + + defaultVariants: { + size: '2', + variant: 'gray', + shape: 'circle', + }, +}); + +export const avatarImage = style({ + display: 'flex', + objectFit: 'cover', + boxSizing: 'border-box', + height: '100%', + verticalAlign: 'middle', + width: '100%', +}); + +const avatarFallbackBase = style({ + textTransform: 'uppercase', + fontFamily: tokens.fonts.rubik, + color: tokens.colors.deepBlue['10'], +}); + +export const avatarFallbackRecipe = recipe({ + base: avatarFallbackBase, + + variants: { + size: { + '1': { + fontSize: '10px', + lineHeight: '15px', + }, + '2': { + fontSize: tokens.fontSizes['3'], + }, + '3': { + fontSize: tokens.fontSizes['6'], + }, + '4': { + fontSize: tokens.fontSizes['7'], + }, + '5': { + fontSize: tokens.fontSizes['8'], + }, + '6': { + fontSize: tokens.fontSizes['9'], + }, + }, + }, + + defaultVariants: { + size: '2', + }, +}); diff --git a/components/Avatar/Avatar.vanilla.test.tsx b/components/Avatar/Avatar.vanilla.test.tsx new file mode 100644 index 00000000..ac92f4c1 --- /dev/null +++ b/components/Avatar/Avatar.vanilla.test.tsx @@ -0,0 +1,212 @@ +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; +import React from 'react'; + +import { VanillaExtractThemeProvider } from '../../styles/themeContext'; +import { AvatarVanilla } from './Avatar.vanilla'; + +describe('AvatarVanilla', () => { + const renderWithTheme = (ui: React.ReactElement) => { + return render({ui}); + }; + + it('should render correctly', () => { + const { container } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should render with image source', () => { + const { container } = renderWithTheme( + , + ); + + expect(container.firstChild).toBeInTheDocument(); + const img = container.querySelector('img'); + if (img) { + expect(img).toHaveAttribute('src', 'https://picsum.photos/100'); + expect(img).toHaveAttribute('alt', 'Test Avatar'); + } + }); + + it('should render fallback when no image source', () => { + const { container } = renderWithTheme(); + const wrapper = container.firstChild; + + expect(wrapper).toHaveTextContent('AB'); + }); + + it('should apply size variants', () => { + const sizes = ['1', '2', '3', '4', '5', '6'] as const; + + sizes.forEach((size) => { + const { container, unmount } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + + it('should apply variant prop', () => { + const variants = ['gray', 'red', 'purple', 'blue', 'green', 'orange'] as const; + + variants.forEach((variant) => { + const { container, unmount } = renderWithTheme( + , + ); + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + + it('should apply shape prop', () => { + const shapes = ['square', 'circle'] as const; + + shapes.forEach((shape) => { + const { container, unmount } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + + it('should combine multiple variant props', () => { + const { container } = renderWithTheme( + , + ); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should render with custom className', () => { + const { container } = renderWithTheme(); + const wrapper = container.firstChild; + const avatarRoot = wrapper?.firstChild as HTMLElement; + + expect(avatarRoot.className).toContain('custom-avatar'); + }); + + it('should apply custom styles via style prop', () => { + const { container } = renderWithTheme(); + const wrapper = container.firstChild as HTMLElement; + + expect(wrapper.style.border).toBe('2px solid red'); + }); + + it('should apply CSS prop styles', () => { + const { container } = renderWithTheme(); + const wrapper = container.firstChild as HTMLElement; + + expect(wrapper.style.padding).toBe('20px'); + expect(wrapper.style.margin).toBe('8px'); + }); + + it('should merge style and css props correctly', () => { + const { container } = renderWithTheme( + , + ); + const wrapper = container.firstChild as HTMLElement; + + expect(wrapper.style.border).toBe('1px solid blue'); + expect(wrapper.style.padding).toBe('30px'); + expect(wrapper.style.margin).toBe('8px'); + }); + + it('should apply wrapper styles including custom css', () => { + const { container } = renderWithTheme(); + const wrapper = container.firstChild as HTMLElement; + const avatarRoot = wrapper.firstChild as HTMLElement; + + expect(wrapper).toBeInTheDocument(); + expect(avatarRoot).toBeInTheDocument(); + + // Custom padding from css prop should be applied to wrapper + expect(wrapper.style.padding).toBe('20px'); + const styleAttr = wrapper.getAttribute('style'); + expect(styleAttr).toContain('position'); + expect(styleAttr).toContain('padding: 20px'); + }); + + it('should forward ref correctly', () => { + const ref = React.createRef(); + renderWithTheme(); + + expect(ref.current).not.toBeNull(); + }); + + it('should pass through HTML attributes', () => { + const { container } = renderWithTheme( + , + ); + const wrapper = container.firstChild; + const avatarRoot = wrapper?.firstChild as HTMLElement; + + expect(avatarRoot.getAttribute('data-testid')).toBe('test-avatar'); + expect(avatarRoot.getAttribute('aria-label')).toBe('Test Avatar'); + }); + + it('should have no accessibility violations with image', async () => { + const { container } = renderWithTheme( + , + ); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + it('should have no accessibility violations with fallback', async () => { + const { container } = renderWithTheme(); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + it('should work with light theme', () => { + const { container } = render( + + + , + ); + + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should work with dark theme', () => { + const { container } = render( + + + , + ); + + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should work with different primary colors', () => { + const primaryColors = ['neon', 'blue', 'orange', 'red', 'green'] as const; + + primaryColors.forEach((color) => { + const { container, unmount } = render( + + + , + ); + + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + + it('should apply default variants when none specified', () => { + const { container } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should handle React node as fallback', () => { + const { container } = renderWithTheme( + Custom} />, + ); + const fallback = container.querySelector('[data-testid="custom-fallback"]'); + + expect(fallback).toBeInTheDocument(); + expect(fallback).toHaveTextContent('Custom'); + }); +}); diff --git a/components/Avatar/Avatar.vanilla.tsx b/components/Avatar/Avatar.vanilla.tsx new file mode 100644 index 00000000..eff6a9eb --- /dev/null +++ b/components/Avatar/Avatar.vanilla.tsx @@ -0,0 +1,52 @@ +import * as AvatarPrimitive from '@radix-ui/react-avatar'; +import { RecipeVariants } from '@vanilla-extract/recipes'; +import React, { forwardRef } from 'react'; + +import { CSSProps } from '../../styles/cssProps'; +import { BoxVanilla } from '../Box/Box.vanilla'; +import { avatarFallbackRecipe, avatarImage, avatarRecipe } from './Avatar.vanilla.css'; + +type AvatarRecipeVariants = NonNullable>; + +interface AvatarOwnProps extends AvatarRecipeVariants, CSSProps { + alt?: string; + src?: string; + fallback?: React.ReactNode; +} + +export type AvatarVanillaProps = Omit< + React.ComponentPropsWithoutRef, + keyof AvatarOwnProps +> & + AvatarOwnProps; + +const AvatarVanillaComponent = forwardRef< + React.ElementRef, + AvatarVanillaProps +>(({ alt, src, fallback, size, variant, shape, css, style, ...props }, ref) => { + const recipeClass = avatarRecipe({ size, variant, shape }); + const fallbackRecipeClass = avatarFallbackRecipe({ size }); + + // This CSS will be processed by the Box component + const wrapperCss = { + ...css, + position: 'relative' as const, + height: 'fit-content' as const, + width: 'fit-content' as const, + }; + + return ( + + + + + {fallback} + + + + ); +}); + +AvatarVanillaComponent.displayName = 'AvatarVanilla'; + +export const AvatarVanilla = AvatarVanillaComponent; diff --git a/components/Avatar/index.ts b/components/Avatar/index.ts index 27700fe3..6964cd4a 100644 --- a/components/Avatar/index.ts +++ b/components/Avatar/index.ts @@ -1 +1,3 @@ export * from './Avatar'; +export type { AvatarVanillaProps } from './Avatar.vanilla'; +export { AvatarVanilla } from './Avatar.vanilla'; diff --git a/components/Bubble/Bubble.stories.tsx b/components/Bubble/Bubble.stories.tsx index 6f62cefd..8c8ec2e7 100644 --- a/components/Bubble/Bubble.stories.tsx +++ b/components/Bubble/Bubble.stories.tsx @@ -4,8 +4,12 @@ import React from 'react'; import { VariantProps } from '../../stitches.config'; import { modifyVariantsForStory } from '../../utils/modifyVariantsForStory'; +import { BoxVanilla } from '../Box/Box.vanilla'; import { Flex } from '../Flex'; +import { FlexVanilla } from '../Flex/Flex.vanilla'; +import { H3Vanilla } from '../Heading'; import { Bubble } from './Bubble'; +import { BubbleVanilla } from './Bubble.vanilla'; type BubbleVariants = VariantProps; type BubbleProps = BubbleVariants & NonNullable; @@ -66,4 +70,55 @@ Sizes.argTypes = { }, }; +export const Comparison: StoryFn = () => ( + + + Stitches Version + + + + + + + + + + + + + + + + + + + + + + + Vanilla Extract Version + + + + + + + + + + + + + + + + + + + + + + +); + export default Component; diff --git a/components/Bubble/Bubble.vanilla.css.ts b/components/Bubble/Bubble.vanilla.css.ts new file mode 100644 index 00000000..3b2eb743 --- /dev/null +++ b/components/Bubble/Bubble.vanilla.css.ts @@ -0,0 +1,144 @@ +import { keyframes, style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; + +import { tokens } from '../../styles/tokens.css'; + +const bip = keyframes({ + '0%': { + top: 0, + right: 0, + bottom: 0, + left: 0, + opacity: 1, + }, + '100%': { + top: -8, + right: -8, + bottom: -8, + left: -8, + opacity: 0, + }, +}); + +const baseBubble = style({ + display: 'inline-block', + width: tokens.sizes['4'], + height: tokens.sizes['4'], + backgroundColor: tokens.colors.red['8'], + borderRadius: '50%', + position: 'relative', + + selectors: { + '&::before': { + animation: `${bip} 1s ease infinite`, + boxSizing: 'border-box', + content: '""', + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, + boxShadow: 'inset 0 0 0 1px rgba(255,255,255,.5)', + borderRadius: '50%', + pointerEvents: 'none', + zIndex: -1, + }, + '&::after': { + boxSizing: 'border-box', + content: '""', + position: 'absolute', + top: 5, + right: 5, + bottom: 5, + left: 5, + backgroundColor: 'rgba(255,255,255,.1)', + borderRadius: '50%', + pointerEvents: 'none', + }, + }, +}); + +export const bubbleRecipe = recipe({ + base: baseBubble, + + variants: { + variant: { + red: { + backgroundColor: tokens.colors.red['8'], + selectors: { + '&::before': { backgroundColor: tokens.colors.red['8'] }, + }, + }, + green: { + backgroundColor: tokens.colors.green['8'], + selectors: { + '&::before': { backgroundColor: tokens.colors.green['8'] }, + }, + }, + orange: { + backgroundColor: tokens.colors.orange['8'], + selectors: { + '&::before': { backgroundColor: tokens.colors.orange['8'] }, + }, + }, + blue: { + backgroundColor: tokens.colors.blue['8'], + selectors: { + '&::before': { backgroundColor: tokens.colors.blue['8'] }, + }, + }, + yellow: { + backgroundColor: tokens.colors.neon['8'], + selectors: { + '&::before': { backgroundColor: tokens.colors.neon['8'] }, + }, + }, + purple: { + backgroundColor: tokens.colors.purple['8'], + selectors: { + '&::before': { backgroundColor: tokens.colors.purple['8'] }, + }, + }, + gray: { + backgroundColor: tokens.colors.slate['8'], + selectors: { + '&::before': { backgroundColor: tokens.colors.slate['8'] }, + }, + }, + }, + size: { + 'x-small': { + width: tokens.sizes['1'], + height: tokens.sizes['1'], + }, + small: { + width: tokens.sizes['2'], + height: tokens.sizes['2'], + }, + medium: { + width: tokens.sizes['3'], + height: tokens.sizes['3'], + }, + large: { + width: tokens.sizes['4'], + height: tokens.sizes['4'], + }, + 'x-large': { + width: tokens.sizes['5'], + height: tokens.sizes['5'], + }, + }, + noAnimation: { + true: { + selectors: { + '&::before': { content: 'none' }, + }, + }, + }, + }, + + defaultVariants: { + size: 'small', + noAnimation: false, + }, +}); diff --git a/components/Bubble/Bubble.vanilla.test.tsx b/components/Bubble/Bubble.vanilla.test.tsx new file mode 100644 index 00000000..d7c0e3c5 --- /dev/null +++ b/components/Bubble/Bubble.vanilla.test.tsx @@ -0,0 +1,201 @@ +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; +import React from 'react'; + +import { VanillaExtractThemeProvider } from '../../styles/themeContext'; +import { BubbleVanilla } from './Bubble.vanilla'; + +describe('BubbleVanilla', () => { + const renderWithTheme = (ui: React.ReactElement) => { + return render({ui}); + }; + + it('should render correctly', () => { + const { container } = renderWithTheme(); + const bubble = container.firstChild; + + expect(bubble?.nodeName).toBe('DIV'); + expect(bubble).toBeInTheDocument(); + }); + + it('should render with custom className', () => { + const { container } = renderWithTheme(); + const bubble = container.firstChild as HTMLElement; + + expect(bubble.className).toContain('custom-bubble'); + }); + + it('should apply variant prop', () => { + const variants = ['red', 'green', 'orange', 'blue', 'yellow', 'purple', 'gray'] as const; + + variants.forEach((variant) => { + const { container, unmount } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + + it('should apply size prop', () => { + const sizes = ['x-small', 'small', 'medium', 'large', 'x-large'] as const; + + sizes.forEach((size) => { + const { container, unmount } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + + it('should apply noAnimation prop', () => { + const { container } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should combine multiple variant props', () => { + const { container } = renderWithTheme( + , + ); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should apply custom styles via style prop', () => { + const { container } = renderWithTheme( + , + ); + const bubble = container.firstChild as HTMLElement; + + expect(bubble.style.backgroundColor).toBe('red'); + expect(bubble.style.padding).toBe('10px'); + }); + + it('should apply CSS prop styles', () => { + const { container } = renderWithTheme(); + const bubble = container.firstChild as HTMLElement; + + expect(bubble.style.padding).toBe('20px'); + expect(bubble.style.margin).toBe('8px'); + }); + + it('should merge style and css props correctly', () => { + const { container } = renderWithTheme( + , + ); + const bubble = container.firstChild as HTMLElement; + + expect(bubble.style.backgroundColor).toBe('blue'); + expect(bubble.style.padding).toBe('30px'); + expect(bubble.style.margin).toBe('8px'); + }); + + it('should forward ref correctly', () => { + const ref = { current: null }; + renderWithTheme(); + + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + + it('should pass through HTML attributes', () => { + const { container } = renderWithTheme( + , + ); + const bubble = container.firstChild as HTMLElement; + + expect(bubble.getAttribute('data-testid')).toBe('test-bubble'); + expect(bubble.getAttribute('aria-label')).toBe('Test Bubble'); + }); + + it('should have no accessibility violations', async () => { + const { container } = renderWithTheme(); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + it('should work with light theme', () => { + const { container } = render( + + + , + ); + + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should work with dark theme', () => { + const { container } = render( + + + , + ); + + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should work with different primary colors', () => { + const primaryColors = ['neon', 'blue', 'orange', 'red', 'green'] as const; + + primaryColors.forEach((primaryColor) => { + const { container, unmount } = render( + + + , + ); + + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + + it('should render with all variant combinations in light theme', () => { + const variants = ['red', 'green', 'orange', 'blue', 'yellow', 'purple', 'gray'] as const; + const sizes = ['x-small', 'small', 'medium', 'large', 'x-large'] as const; + + variants.forEach((variant) => { + sizes.forEach((size) => { + const { container, unmount } = render( + + + , + ); + + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + }); + + it('should render with all variant combinations in dark theme', () => { + const variants = ['red', 'green', 'orange', 'blue', 'yellow', 'purple', 'gray'] as const; + const sizes = ['x-small', 'small', 'medium', 'large', 'x-large'] as const; + + variants.forEach((variant) => { + sizes.forEach((size) => { + const { container, unmount } = render( + + + , + ); + + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); + }); + + it('should render with animation disabled', () => { + const { container } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should handle all size variants with noAnimation', () => { + const sizes = ['x-small', 'small', 'medium', 'large', 'x-large'] as const; + + sizes.forEach((size) => { + const { container, unmount } = renderWithTheme(); + expect(container.firstChild).toBeInTheDocument(); + unmount(); + }); + }); +}); diff --git a/components/Bubble/Bubble.vanilla.tsx b/components/Bubble/Bubble.vanilla.tsx new file mode 100644 index 00000000..eddb0620 --- /dev/null +++ b/components/Bubble/Bubble.vanilla.tsx @@ -0,0 +1,57 @@ +import { assignInlineVars } from '@vanilla-extract/dynamic'; +import { RecipeVariants } from '@vanilla-extract/recipes'; +import { ElementType, forwardRef } from 'react'; + +import { CSSProps, processCSSProp } from '../../styles/cssProps'; +import { + PolymorphicComponent, + PolymorphicComponentProps, + PolymorphicRef, +} from '../../styles/polymorphic'; +import { useVanillaExtractTheme } from '../../styles/themeContext'; +import { bubbleRecipe } from './Bubble.vanilla.css'; + +type BubbleRecipeVariants = NonNullable>; + +interface BubbleOwnProps extends BubbleRecipeVariants, CSSProps {} + +export type BubbleVanillaProps = PolymorphicComponentProps< + C, + BubbleOwnProps +>; + +type BubbleVanillaComponent = PolymorphicComponent<'div', BubbleVanillaProps>; + +const BubbleVanillaComponentImpl = forwardRef( + ( + { as, className, css, style, size, variant, noAnimation, ...props }: BubbleVanillaProps, + ref?: PolymorphicRef, + ) => { + const Component = as || 'div'; + + const { colors } = useVanillaExtractTheme(); + + const { style: cssStyles, vars } = processCSSProp(css, colors); + + const mergedStyles = { + ...cssStyles, + ...style, + ...assignInlineVars(vars), + }; + + const recipeClass = bubbleRecipe({ size, variant, noAnimation }); + + return ( + + ); + }, +); + +BubbleVanillaComponentImpl.displayName = 'BubbleVanilla'; + +export const BubbleVanilla = BubbleVanillaComponentImpl as BubbleVanillaComponent; diff --git a/components/Bubble/index.ts b/components/Bubble/index.ts index c6422469..2a44cffd 100644 --- a/components/Bubble/index.ts +++ b/components/Bubble/index.ts @@ -1 +1,3 @@ export * from './Bubble'; +export type { BubbleVanillaProps } from './Bubble.vanilla'; +export { BubbleVanilla } from './Bubble.vanilla'; diff --git a/index.ts b/index.ts index d1573f51..1efb2e19 100644 --- a/index.ts +++ b/index.ts @@ -16,13 +16,15 @@ export { Thead as AriaThead, Tr as AriaTr, } from './components/AriaTable'; -export { Avatar } from './components/Avatar'; +export type { AvatarVanillaProps } from './components/Avatar'; +export { Avatar, AvatarVanilla } from './components/Avatar'; export type { BadgeVanillaProps } from './components/Badge'; export { Badge, BADGE_COLORS_VANILLA, BadgeVanilla } from './components/Badge'; export type { BlockquoteVanillaProps } from './components/Blockquote'; export { Blockquote, BlockquoteVanilla } from './components/Blockquote'; export { Box, BoxVanilla } from './components/Box'; -export { Bubble } from './components/Bubble'; +export type { BubbleVanillaProps } from './components/Bubble'; +export { Bubble, BubbleVanilla } from './components/Bubble'; export type { ButtonVanillaProps } from './components/Button'; export { Button, ButtonVanilla } from './components/Button'; export {