From 1ed9e0e135828938b5950daec769a929053bc637 Mon Sep 17 00:00:00 2001 From: astavitskaya Date: Fri, 12 Dec 2025 15:28:50 -0500 Subject: [PATCH 01/15] test: rokt catalog nav poc --- .../rokt-catalog/ExpandedSidebar.stories.tsx | 161 ++++++++++++++++ .../POC/rokt-catalog/ExpandedSidebar.tsx | 175 ++++++++++++++++++ .../POC/rokt-catalog/SidebarHeader.tsx | 30 +++ .../POC/rokt-catalog/SidebarUserProfile.tsx | 105 +++++++++++ .../POC/rokt-catalog/expanded-sidebar.css | 47 +++++ src/components/POC/rokt-catalog/rokt-logo.svg | 56 ++++++ 6 files changed, 574 insertions(+) create mode 100644 src/components/POC/rokt-catalog/ExpandedSidebar.stories.tsx create mode 100644 src/components/POC/rokt-catalog/ExpandedSidebar.tsx create mode 100644 src/components/POC/rokt-catalog/SidebarHeader.tsx create mode 100644 src/components/POC/rokt-catalog/SidebarUserProfile.tsx create mode 100644 src/components/POC/rokt-catalog/expanded-sidebar.css create mode 100644 src/components/POC/rokt-catalog/rokt-logo.svg diff --git a/src/components/POC/rokt-catalog/ExpandedSidebar.stories.tsx b/src/components/POC/rokt-catalog/ExpandedSidebar.stories.tsx new file mode 100644 index 0000000000..9b1791461b --- /dev/null +++ b/src/components/POC/rokt-catalog/ExpandedSidebar.stories.tsx @@ -0,0 +1,161 @@ +import { type Meta, type StoryObj } from '@storybook/react' +import React, { useState } from 'react' +import { + HomeOutlined, + SettingOutlined, + TagOutlined, + ShoppingCartOutlined, + ShopOutlined, + StarFilled, + AimOutlined, + FileTextOutlined, + InboxOutlined, + FolderOutlined, + SendOutlined, + LineChartOutlined, +} from '@ant-design/icons' +import { Avatar } from 'src/components' +import { ColorBorder, MpBrandSecondary5 } from 'src/styles/style' +import { ExpandedSidebar, type IExpandedSidebarItem, type IExpandedSidebarSection } from './ExpandedSidebar' + +// Simple gray avatar for additional partners +const GrayAvatar = () => ( + +) + +// Navigation items matching the Figma design +const roktNavItems: IExpandedSidebarItem[] = [ + { key: 'home', icon: , label: 'Home' }, + { key: 'settings', icon: , label: 'Settings' }, + { key: 'products', icon: , label: 'Products' }, + { key: 'orders', icon: , label: 'Orders' }, +] + +// Featured partners (polished) +const featuredPartners: IExpandedSidebarItem[] = [ + { + key: 'nordstrom', + icon: ( + + N + + ), + label: 'Nordstrom', + }, + { + key: 'macys', + icon: ( + + + + ), + label: "Macy's", + }, + { + key: 'target', + icon: ( + + + + ), + label: 'Target', + }, +] + +// Additional partners (simple gray avatars) +const additionalPartners: IExpandedSidebarItem[] = [ + { key: 'shopsimon', icon: , label: 'ShopSimon' }, + { key: 'canal-demo', icon: , label: 'Canal Demo Store' }, + { key: 'forex', icon: , label: 'Forex (Deactivated)' }, + { key: 'fellow', icon: , label: 'Fellow (Deactivated)' }, + { key: 'alyssa', icon: , label: 'Alyssa Milano (Deact...' }, + { key: 'flip', icon: , label: 'Flip (Deactivated)' }, +] + +// Function to build marketplace items with dynamic partners +const buildMarketplaceItems = (expanded: boolean, onToggle: () => void): IExpandedSidebarItem[] => [ + { + key: 'partners', + icon: , + label: 'Partners', + children: [ + ...featuredPartners, + ...(expanded ? additionalPartners : []), + { + key: 'see-toggle', + label: expanded ? 'See less' : 'See more', + onClick: onToggle, + }, + ], + }, + { key: 'proposals', icon: , label: 'Proposals', badge: 10 }, + { key: 'inbox', icon: , label: 'Inbox', badge: 10 }, + { + key: 'reports', + icon: , + label: 'Reports', + children: [ + { key: 'analytics', label: 'Analytics' }, + { key: 'payouts', label: 'Payouts' }, + ], + }, +] + +// Ads section items +const adsItems: IExpandedSidebarItem[] = [ + { key: 'campaign-manager', icon: , label: 'Campaign manager' }, + { key: 'ads-analytics', icon: , label: 'Analytics' }, +] + +// Function to build sections with dynamic marketplace items +const buildSections = (expanded: boolean, onToggle: () => void): IExpandedSidebarSection[] => [ + { label: 'MARKETPLACE', items: buildMarketplaceItems(expanded, onToggle) }, + { label: 'ADS', items: adsItems }, +] + +const meta: Meta = { + title: 'POC/Rokt Catalog/ExpandedSidebar', + component: ExpandedSidebar, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + Story => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +/** + * Primary story matching the Rokt Catalog Figma design + */ +export const RoktCatalog: Story = { + render: () => { + const [expanded, setExpanded] = useState(false) + + return ( + setExpanded(!expanded))} + userProfile={{ + name: 'lily riojas', + email: 'lily@sundaycitizen.co', + isOnline: true, + onClick: () => alert('User profile clicked!'), + }} + selectedKey="home" + defaultOpenKeys={['partners', 'reports']} + /> + ) + }, +} diff --git a/src/components/POC/rokt-catalog/ExpandedSidebar.tsx b/src/components/POC/rokt-catalog/ExpandedSidebar.tsx new file mode 100644 index 0000000000..c9282984bc --- /dev/null +++ b/src/components/POC/rokt-catalog/ExpandedSidebar.tsx @@ -0,0 +1,175 @@ +import './expanded-sidebar.css' +import React, { type ReactNode } from 'react' +import { Badge, Flex, Layout, Menu, Typography, type IMenuProps } from 'src/components' +import { SidebarHeader, type ISidebarHeaderProps } from './SidebarHeader' +import { SidebarUserProfile, type ISidebarUserProfileProps } from './SidebarUserProfile' +import { ColorBorder, ColorBgLayout, ColorText, Padding } from 'src/styles/style' + +type MenuProps = IMenuProps + +export interface IExpandedSidebarItem { + key: string + label: ReactNode + icon?: ReactNode + badge?: number + children?: IExpandedSidebarItem[] + onClick?: () => void + href?: string + disabled?: boolean +} + +export interface IExpandedSidebarSection { + label: string + items: IExpandedSidebarItem[] +} + +export interface IExpandedSidebarProps { + header?: ISidebarHeaderProps + items?: IExpandedSidebarItem[] + sections?: IExpandedSidebarSection[] + userProfile?: ISidebarUserProfileProps + selectedKey?: string + defaultOpenKeys?: string[] + onSelect?: (key: string) => void + onOpenChange?: (openKeys: string[]) => void + width?: number + className?: string +} + +export function ExpandedSidebar({ + header, + items = [], + sections = [], + userProfile, + selectedKey, + defaultOpenKeys = [], + onSelect, + onOpenChange, + width = 280, + className = '', +}: IExpandedSidebarProps) { + const handleSelect: MenuProps['onSelect'] = ({ key }) => { + onSelect?.(key) + } + + const handleOpenChange: MenuProps['onOpenChange'] = openKeys => { + onOpenChange?.(openKeys) + } + + // Convert our items to Ant Menu items format + const convertToMenuItems = (navItems: IExpandedSidebarItem[]): MenuProps['items'] => { + return navItems.map(item => { + const baseItem: NonNullable[number] = { + key: item.key, + icon: item.icon, + disabled: item.disabled, + onClick: item.onClick, + } + + // Handle items with badges + if (item.badge !== undefined) { + return { + ...baseItem, + label: ( + + {item.label} + + + ), + } + } + + // Handle items with children (submenus) + if (item.children && item.children.length > 0) { + return { + ...baseItem, + label: item.label, + children: convertToMenuItems(item.children), + } + } + + // Regular items + return { + ...baseItem, + label: item.label, + } + }) + } + + // Build the complete menu items array + const buildMenuItems = (): MenuProps['items'] => { + const menuItems: MenuProps['items'] = [] + + // Add top-level items first + if (items.length > 0) { + menuItems.push(...(convertToMenuItems(items) ?? [])) + } + + // Add sections with dividers and group labels + sections.forEach((section, index) => { + // Add divider before section (except for first section if no top items) + if (items.length > 0 || index > 0) { + menuItems.push({ + type: 'divider', + key: `divider-${section.label}`, + }) + } + + // Add section group with its items + menuItems.push({ + type: 'group', + label: ( + + {section.label} + + ), + key: `group-${section.label}`, + children: convertToMenuItems(section.items), + }) + }) + + return menuItems + } + + return ( + + {header && } + + + + {userProfile && } + + ) +} + +export type { ISidebarHeaderProps, ISidebarUserProfileProps } diff --git a/src/components/POC/rokt-catalog/SidebarHeader.tsx b/src/components/POC/rokt-catalog/SidebarHeader.tsx new file mode 100644 index 0000000000..e7cfb07d9c --- /dev/null +++ b/src/components/POC/rokt-catalog/SidebarHeader.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { Flex, Typography } from 'src/components' +import roktLogo from './rokt-logo.svg' +import { SizeSm, Margin, ColorTextSecondary } from 'src/styles/style' + +export interface ISidebarHeaderProps { + title: string + subtitle?: string + onClick?: () => void +} + +export function SidebarHeader({ title, subtitle, onClick }: ISidebarHeaderProps) { + return ( + + Rokt + + + {title} + + {subtitle && ( + {subtitle} + )} + + + ) +} diff --git a/src/components/POC/rokt-catalog/SidebarUserProfile.tsx b/src/components/POC/rokt-catalog/SidebarUserProfile.tsx new file mode 100644 index 0000000000..999bd492f7 --- /dev/null +++ b/src/components/POC/rokt-catalog/SidebarUserProfile.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react' +import { Avatar, Badge, Dropdown, Flex, Icon, Typography, type IMenuProps } from 'src/components' +import { + UserOutlined, + SettingOutlined, + QuestionCircleOutlined, + ShoppingOutlined, + SwapOutlined, + ShopOutlined, + FormOutlined, + LogoutOutlined, +} from '@ant-design/icons' +import { ColorBorder, PaddingSm } from 'src/styles/style' + +const { Text } = Typography + +export interface IUserMenuItem { + key: string + label: string + icon?: React.ReactNode + onClick?: () => void + danger?: boolean +} + +export interface ISidebarUserProfileProps { + name: string + email: string + avatarSrc?: string + avatarAlt?: string + isOnline?: boolean + onClick?: () => void + menuItems?: IUserMenuItem[] +} + +const defaultMenuItems: IUserMenuItem[] = [ + { key: 'profile', label: 'View & edit profile', icon: }, + { key: 'settings', label: 'Settings', icon: }, + { key: 'help', label: 'Help center', icon: }, + { key: 'test-order', label: 'Run a test order', icon: }, + { key: 'switch-accounts', label: 'Switch accounts', icon: }, + { key: 'switch-partner', label: 'Switch to Partner', icon: }, + { key: 'feedback', label: 'Give feedback', icon: }, + { key: 'signout', label: 'Sign out', icon: }, +] + +export function SidebarUserProfile({ + name, + email, + avatarSrc, + avatarAlt, + isOnline = true, + onClick, + menuItems = defaultMenuItems, +}: ISidebarUserProfileProps) { + const [open, setOpen] = useState(false) + + const dropdownItems: IMenuProps['items'] = menuItems.map(item => ({ + key: item.key, + label: item.label, + icon: item.icon, + danger: item.danger, + onClick: item.onClick, + })) + + const userProfileContent = ( + + + + + {!avatarSrc && name.charAt(0).toUpperCase()} + + + + {name} + {email} + + + + + + + + ) + + return ( + + {userProfileContent} + + ) +} diff --git a/src/components/POC/rokt-catalog/expanded-sidebar.css b/src/components/POC/rokt-catalog/expanded-sidebar.css new file mode 100644 index 0000000000..f95a0e8c4e --- /dev/null +++ b/src/components/POC/rokt-catalog/expanded-sidebar.css @@ -0,0 +1,47 @@ +/* Menu item padding overrides */ +.expandedSidebar__menu .ant-menu-item { + padding-left: var(--padding-sm) !important; + padding-right: var(--padding-sm) !important; + margin: 0 !important; +} + +.expandedSidebar__menu .ant-menu-submenu-title { + padding-left: var(--padding-sm) !important; + padding-right: var(--padding-sm) !important; + margin: 0 !important; +} + +.expandedSidebar__menu .ant-menu-item-group-title { + padding-left: var(--padding-sm) !important; + padding-right: var(--padding-sm) !important; + + /* Let Typography.Text handle font styles */ + font-size: inherit !important; + font-weight: inherit !important; + color: inherit !important; +} + +.expandedSidebar__menu .ant-menu-sub .ant-menu-item { + padding-left: calc(var(--padding-md) + 32px) !important; +} + +.expandedSidebar__menu .ant-menu-sub { + background: transparent !important; +} + +/* Ensure Sider children container fills height */ +.ant-layout-sider-children { + display: flex !important; + flex-direction: column !important; +} + +/* User profile styles */ +.expandedSidebar__userProfile { + margin-top: auto; +} + +.expandedSidebar__avatarWrapper { + position: relative; + flex-shrink: 0; +} + diff --git a/src/components/POC/rokt-catalog/rokt-logo.svg b/src/components/POC/rokt-catalog/rokt-logo.svg new file mode 100644 index 0000000000..a4163f2925 --- /dev/null +++ b/src/components/POC/rokt-catalog/rokt-logo.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 9977293a9db7e053001a8920dee38efab3d5bb59 Mon Sep 17 00:00:00 2001 From: skosemP Date: Mon, 15 Dec 2025 23:14:31 -0500 Subject: [PATCH 02/15] docs: refresh button docs (#694) --- .../General/Button/Documentation.mdx | 81 ++++----- .../general/Button/Button.stories.tsx | 157 ++++++++++++++++-- 2 files changed, 169 insertions(+), 69 deletions(-) diff --git a/docs/components/General/Button/Documentation.mdx b/docs/components/General/Button/Documentation.mdx index 593c50e283..244cfd710f 100644 --- a/docs/components/General/Button/Documentation.mdx +++ b/docs/components/General/Button/Documentation.mdx @@ -1,4 +1,4 @@ -import { Meta, Canvas } from '@storybook/blocks' +import { Meta, Canvas, Story } from '@storybook/blocks' import * as ButtonStories from '../../../../src/components/general/Button/Button.stories' @@ -8,82 +8,59 @@ import * as ButtonStories from '../../../../src/components/general/Button/Button # Button -#### Overview +### Overview The **Button** component is used to trigger actions or navigate the interface. It supports various styles, sizes, and states to fit different use cases. -#### Key Features +### Button Types -- **Variants:** Includes primary, default, dashed, text, and link buttons for different use cases. -- **Sizes:** Supports small, medium (default), and large sizes. -- **States:** Disabled, loading, and hover states to indicate action availability. -- **Icons:** Easily integrate icons for visual clarity and emphasis. +- **Primary** – High-emphasis actions like submitting or saving workflows. +- **Secondary** – Supportive tasks such as canceling or opening supplemental dialogs. +- **Tertiary** – Lower-priority options that should sit behind primary/secondary CTAs. +- **Text** – Quiet inline affordances that should blend into surrounding content. +- **Link** – Navigation-style actions that resemble hyperlinks within layouts. + -### Button Variants and Use Cases + -#### Primary Button +### Button sizes -**Use Case**: For the main action on a page or section. -- **Example:** Submitting a form, saving changes, or initiating a key process. +Buttons ship in three heights to support different layouts. The default is `middle`, shown alongside the smaller and larger options below. -**Examples**: + -- **With Icon**: - +### Buttons with icons -- **Without Icon**: - +The icon-enabled variants mirror the same hierarchy while reinforcing meaning with glyphs. -#### Default Button + -**Use Case**: For secondary actions that complement the primary task. -- **Example:** Canceling an action, opening a dialog, or performing less important tasks. +#### Icon-only buttons -**Examples**: +Icon-only buttons provide compact controls for toolbars and quick actions where text isn’t necessary. -- **With Icon**: - - -- **Without Icon**: - - -#### Dashed Button - -**Use Case**: For tertiary actions in the visual hierarchy. - -**Examples**: - -- **With Icon**: - - -- **Without Icon**: - + -#### Link Button +### Loading state -**Use Case**: Ideal for triggering an actions that don't require heavy visual emphasis. -- **Example:** Viewing errors, navigating to documentation -When navigating to a new URL or external page, always use [Typography.Link](https://mparticle.github.io/aquarium/?path=/story/components-general-typography-link--primary) instead of a Link Button. +The loading variant keeps the user informed while an action finishes processing. Use concise labels that set expectations. -**Examples**: + -- **With Icon**: - +### Loading text button -- **Without Icon**: - +Use the text treatment for subtle “load more” affordances that need to display progress without taking extra visual weight. -#### Icon-Only Button + -**Use Case**: For compact controls where only an icon is needed. -- **Example:** Toolbar controls, close buttons, or quick actions. +### Destructive action -**Examples**: - +Use the danger style to signal irreversible steps such as deletions. Pair it with clear language so users understand the impact. + -#### Related Links +### Related Links | Type | Resource | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | diff --git a/src/components/general/Button/Button.stories.tsx b/src/components/general/Button/Button.stories.tsx index 3e435afd68..726a1131bf 100644 --- a/src/components/general/Button/Button.stories.tsx +++ b/src/components/general/Button/Button.stories.tsx @@ -1,8 +1,9 @@ import { Button } from 'src/components/general/Button/Button' import { type Meta, type StoryObj } from '@storybook/react' import { userEvent } from '@storybook/test' -import { Divider, Flex, Icon, Typography, Tooltip } from 'src/components' +import { Alert, Flex, Icon, Typography, Tooltip } from 'src/components' import React from 'react' +import { BorderRadiusLg, ColorBorderSecondary, ColorWhite, MarginMd, SizeXs } from 'src/styles/style' const meta: Meta = { title: 'Components/General/Button', @@ -36,6 +37,44 @@ const meta: Meta = { export default meta type Story = StoryObj +export const LinkUsageNote: Story = { + parameters: { + docs: { + source: { + state: 'hidden', + }, + }, + }, + render: () => { + return ( + + 💡 + + Use the{' '} + + Typography.Link + {' '} + when navigating to a new URL or external page. + + + } + /> + ) + }, +} /* Initial story templates generated by AI. @@ -45,7 +84,7 @@ type Story = StoryObj export const Primary: Story = { args: { type: 'primary', - children: 'Create', + children: 'Primary', }, play: async context => { const button = context.canvasElement.querySelector('button') @@ -61,14 +100,14 @@ export const PrimaryWithIcon: Story = { args: { type: 'primary', icon: , - children: 'Add', + children: 'Primary', }, } export const Default: Story = { args: { type: 'default', - children: 'Cancel', + children: 'Secondary', }, } @@ -76,13 +115,14 @@ export const DefaultWithIcon: Story = { args: { type: 'default', icon: , - children: 'View Columns', + children: 'Secondary', }, } export const Dashed: Story = { args: { type: 'dashed', + children: 'Tertiary', }, } @@ -90,14 +130,14 @@ export const DashedWithIcon: Story = { args: { type: 'dashed', icon: , - children: 'Connect Output', + children: 'Tertiary', }, } export const Link: Story = { args: { type: 'link', - children: 'Retry', + children: 'Link', }, } @@ -105,22 +145,105 @@ export const LinkWithIcon: Story = { args: { type: 'link', icon: , - children: 'Add Audience Criteria', + children: 'Link', + }, +} + +export const Text: Story = { + args: { + type: 'text', + children: 'Text', + }, +} + +export const Danger: Story = { + args: { + type: 'default', + danger: true, + children: 'Delete', + }, +} + +export const Loading: Story = { + args: { + type: 'primary', + loading: true, + children: 'Loading', + }, +} + +export const LoadingText: Story = { + args: { + type: 'text', + loading: true, + children: 'Load more', + }, +} + +export const TypesOverview: Story = { + render: () => { + return ( + + + + + + + + ) + }, +} + +export const IconTypesOverview: Story = { + render: () => { + return ( + + + + + + + + ) + }, +} + +export const SizeExamples: Story = { + render: () => { + return ( + + + + + + ) }, } export const IconOnly: Story = { render: () => { return ( - <> - - + + setOpen(false)} + dimensions={dimensions} + metrics={metrics} + onDimensionsChange={setDimensions} + onMetricsChange={setMetrics} + onRenameColumn={handleRename} + onRemoveColumn={handleRemove} + /> + + ) +} + +export const Default: Story = { + render: () => , +} + +const ReorderOnlyTemplate = () => { + const [open, setOpen] = useState(false) + const [dimensions, setDimensions] = useState(defaultDimensions) + const [metrics, setMetrics] = useState(defaultMetrics) + + return ( +
+ + + setOpen(false)} + dimensions={dimensions} + metrics={metrics} + onDimensionsChange={setDimensions} + onMetricsChange={setMetrics} + description="Drag to reorder columns." + /> +
+ ) +} + +export const ReorderOnly: Story = { + render: () => , +} diff --git a/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.tsx b/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.tsx new file mode 100644 index 0000000000..280b8a16d2 --- /dev/null +++ b/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.tsx @@ -0,0 +1,304 @@ +import React, { useState, useCallback } from 'react' +import { Drawer, Icon, MoreActionsButton, type IDrawerProps } from 'src/components' +import { Typography } from 'src/components/general/Typography/Typography' +import './ManageColumnsDrawer.css' + +export interface IColumnItem { + /** + * Unique identifier for the column + */ + id: string + /** + * Display label for the column + */ + label: string +} + +export interface IManageColumnsDrawerProps extends Omit { + /** + * Whether the drawer is open + */ + open: boolean + /** + * Handler called when the drawer is closed + */ + onClose: () => void + /** + * List of dimension columns + */ + dimensions: IColumnItem[] + /** + * List of metric columns + */ + metrics: IColumnItem[] + /** + * Handler called when dimensions are reordered + */ + onDimensionsChange: (dimensions: IColumnItem[]) => void + /** + * Handler called when metrics are reordered + */ + onMetricsChange: (metrics: IColumnItem[]) => void + /** + * Handler called when a column is renamed + */ + onRenameColumn?: (columnId: string, type: 'dimension' | 'metric') => void + /** + * Handler called when a column is removed + */ + onRemoveColumn?: (columnId: string, type: 'dimension' | 'metric') => void + /** + * Custom title for the drawer + */ + title?: string + /** + * Custom description text + */ + description?: string + /** + * Label for the dimensions section + */ + dimensionsLabel?: string + /** + * Label for the metrics section + */ + metricsLabel?: string + /** + * Width of the drawer + */ + width?: number +} + +interface IColumnRowProps { + item: IColumnItem + type: 'dimension' | 'metric' + index: number + onDragStart: (e: React.DragEvent, index: number, type: 'dimension' | 'metric') => void + onDragOver: (e: React.DragEvent, index: number) => void + onDragEnd: () => void + onDrop: (e: React.DragEvent, index: number, type: 'dimension' | 'metric') => void + isDragging: boolean + isDraggedOver: boolean + onRename?: () => void + onRemove?: () => void +} + +const ColumnRow: React.FC = ({ + item, + type, + index, + onDragStart, + onDragOver, + onDragEnd, + onDrop, + isDragging, + isDraggedOver, + onRename, + onRemove, +}) => { + const menuItems = { + items: [ + ...(onRename + ? [ + { + key: 'rename', + label: ( + + + Rename + + ), + onClick: onRename, + }, + ] + : []), + ...(onRemove + ? [ + { + key: 'remove', + className: 'manage-columns-menu-item--remove', + label: ( + + + Remove + + ), + onClick: onRemove, + }, + ] + : []), + ], + } + + const showMoreActions = onRename ?? onRemove + + return ( +
onDragStart(e, index, type)} + onDragOver={e => onDragOver(e, index)} + onDragEnd={onDragEnd} + onDrop={e => onDrop(e, index, type)}> +
+ +
+
+ {item.label} +
+ {showMoreActions && ( +
+ +
+ )} +
+ ) +} + +export const ManageColumnsDrawer: React.FC = ({ + open, + onClose, + dimensions, + metrics, + onDimensionsChange, + onMetricsChange, + onRenameColumn, + onRemoveColumn, + title = 'Manage columns', + description = 'Reorder columns by dragging, or use the dropdown to rename or remove.', + dimensionsLabel = 'Current dimensions', + metricsLabel = 'Current metrics', + width = 440, + ...drawerProps +}) => { + const [dragState, setDragState] = useState<{ + draggedIndex: number | null + draggedType: 'dimension' | 'metric' | null + dragOverIndex: number | null + dragOverType: 'dimension' | 'metric' | null + }>({ + draggedIndex: null, + draggedType: null, + dragOverIndex: null, + dragOverType: null, + }) + + const handleDragStart = useCallback((e: React.DragEvent, index: number, type: 'dimension' | 'metric') => { + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData('text/plain', '') // Required for Firefox + setDragState(prev => ({ + ...prev, + draggedIndex: index, + draggedType: type, + })) + }, []) + + const handleDragOver = useCallback( + (e: React.DragEvent, index: number, type: 'dimension' | 'metric') => { + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + + if (dragState.draggedType === type) { + setDragState(prev => ({ + ...prev, + dragOverIndex: index, + dragOverType: type, + })) + } + }, + [dragState.draggedType], + ) + + const handleDragEnd = useCallback(() => { + setDragState({ + draggedIndex: null, + draggedType: null, + dragOverIndex: null, + dragOverType: null, + }) + }, []) + + const handleDrop = useCallback( + (e: React.DragEvent, dropIndex: number, type: 'dimension' | 'metric') => { + e.preventDefault() + + const { draggedIndex, draggedType } = dragState + + if (draggedIndex === null || draggedType === null || draggedType !== type) { + handleDragEnd() + return + } + + if (draggedIndex === dropIndex) { + handleDragEnd() + return + } + + const items = type === 'dimension' ? [...dimensions] : [...metrics] + const [draggedItem] = items.splice(draggedIndex, 1) + items.splice(dropIndex, 0, draggedItem) + + if (type === 'dimension') { + onDimensionsChange(items) + } else { + onMetricsChange(items) + } + + handleDragEnd() + }, + [dragState, dimensions, metrics, onDimensionsChange, onMetricsChange, handleDragEnd], + ) + + const renderSection = (items: IColumnItem[], type: 'dimension' | 'metric', label: string) => ( +
+
+ + {label} + +
+
+ {items.map((item, index) => ( + handleDragOver(e, index, type)} + onDragEnd={handleDragEnd} + onDrop={handleDrop} + isDragging={dragState.draggedIndex === index && dragState.draggedType === type} + isDraggedOver={dragState.dragOverIndex === index && dragState.dragOverType === type} + onRename={onRenameColumn ? () => onRenameColumn(item.id, type) : undefined} + onRemove={onRemoveColumn ? () => onRemoveColumn(item.id, type) : undefined} + /> + ))} +
+
+ ) + + return ( + + {title} + + } + open={open} + onClose={onClose} + width={width} + className="manage-columns-drawer" + {...drawerProps}> +
+
+ {description} +
+ + {dimensions.length > 0 && renderSection(dimensions, 'dimension', dimensionsLabel)} + {metrics.length > 0 && renderSection(metrics, 'metric', metricsLabel)} +
+
+ ) +} diff --git a/src/components/general/Button/Button.stories.tsx b/src/components/general/Button/Button.stories.tsx index 3e435afd68..726a1131bf 100644 --- a/src/components/general/Button/Button.stories.tsx +++ b/src/components/general/Button/Button.stories.tsx @@ -1,8 +1,9 @@ import { Button } from 'src/components/general/Button/Button' import { type Meta, type StoryObj } from '@storybook/react' import { userEvent } from '@storybook/test' -import { Divider, Flex, Icon, Typography, Tooltip } from 'src/components' +import { Alert, Flex, Icon, Typography, Tooltip } from 'src/components' import React from 'react' +import { BorderRadiusLg, ColorBorderSecondary, ColorWhite, MarginMd, SizeXs } from 'src/styles/style' const meta: Meta = { title: 'Components/General/Button', @@ -36,6 +37,44 @@ const meta: Meta = { export default meta type Story = StoryObj +export const LinkUsageNote: Story = { + parameters: { + docs: { + source: { + state: 'hidden', + }, + }, + }, + render: () => { + return ( + + 💡 + + Use the{' '} + + Typography.Link + {' '} + when navigating to a new URL or external page. + +
+ } + /> + ) + }, +} /* Initial story templates generated by AI. @@ -45,7 +84,7 @@ type Story = StoryObj export const Primary: Story = { args: { type: 'primary', - children: 'Create', + children: 'Primary', }, play: async context => { const button = context.canvasElement.querySelector('button') @@ -61,14 +100,14 @@ export const PrimaryWithIcon: Story = { args: { type: 'primary', icon: , - children: 'Add', + children: 'Primary', }, } export const Default: Story = { args: { type: 'default', - children: 'Cancel', + children: 'Secondary', }, } @@ -76,13 +115,14 @@ export const DefaultWithIcon: Story = { args: { type: 'default', icon: , - children: 'View Columns', + children: 'Secondary', }, } export const Dashed: Story = { args: { type: 'dashed', + children: 'Tertiary', }, } @@ -90,14 +130,14 @@ export const DashedWithIcon: Story = { args: { type: 'dashed', icon: , - children: 'Connect Output', + children: 'Tertiary', }, } export const Link: Story = { args: { type: 'link', - children: 'Retry', + children: 'Link', }, } @@ -105,22 +145,105 @@ export const LinkWithIcon: Story = { args: { type: 'link', icon: , - children: 'Add Audience Criteria', + children: 'Link', + }, +} + +export const Text: Story = { + args: { + type: 'text', + children: 'Text', + }, +} + +export const Danger: Story = { + args: { + type: 'default', + danger: true, + children: 'Delete', + }, +} + +export const Loading: Story = { + args: { + type: 'primary', + loading: true, + children: 'Loading', + }, +} + +export const LoadingText: Story = { + args: { + type: 'text', + loading: true, + children: 'Load more', + }, +} + +export const TypesOverview: Story = { + render: () => { + return ( + + + + + + + + ) + }, +} + +export const IconTypesOverview: Story = { + render: () => { + return ( + + + + + + + + ) + }, +} + +export const SizeExamples: Story = { + render: () => { + return ( + + + + + + ) }, } export const IconOnly: Story = { render: () => { return ( - <> - - + )} + {showApplyButton && ( + + )} + + )} + + + ) +} diff --git a/src/components/data-entry/DimensionPicker/dimension-picker.css b/src/components/data-entry/DimensionPicker/dimension-picker.css new file mode 100644 index 0000000000..4816dec6a7 --- /dev/null +++ b/src/components/data-entry/DimensionPicker/dimension-picker.css @@ -0,0 +1,215 @@ +.dimension-picker { + display: flex; + flex-direction: column; + background: #fff; + border: 1px solid #eceae9; + border-radius: 8px; + min-width: 800px; + max-width: 1000px; + box-shadow: 0 6px 16px 0 rgb(0 0 0 / 8%), 0 3px 6px -4px rgb(0 0 0 / 12%), 0 9px 28px 8px rgb(0 0 0 / 5%); +} + +/* Search Bar */ +.dimension-picker__search { + padding: 8px 12px; + border-bottom: 1px solid #eceae9; +} + +.dimension-picker__search .ant-input-affix-wrapper { + background: #fff; + border-color: #eceae9; +} + +.dimension-picker__search .ant-input-affix-wrapper:hover, +.dimension-picker__search .ant-input-affix-wrapper:focus-within { + border-color: #c3aeff; +} + +/* Main Content */ +.dimension-picker__content { + display: flex; + flex: 1; + min-height: 400px; + max-height: 500px; +} + +/* Categories Panel */ +.dimension-picker__categories { + width: 220px; + border-right: 1px solid #eceae9; + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.dimension-picker__categories-title { + display: block; + padding: 10px 12px 6px; +} + +.dimension-picker__categories-list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + flex: 1; +} + +.dimension-picker__category-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + cursor: pointer; + transition: background-color 0.2s ease, border-left-color 0.2s ease; + border-left: 3px solid transparent; +} + +.dimension-picker__category-item:hover { + background-color: #f5f5f5; +} + +.dimension-picker__category-item--selected { + background-color: #f0f0f0; + border-left-color: #717368; +} + +.dimension-picker__category-label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Items Panel */ +.dimension-picker__items { + flex: 1; + overflow-y: auto; + border-right: 1px solid #eceae9; + min-width: 280px; +} + +.dimension-picker__items-list { + list-style: none; + margin: 0; + padding: 4px 0; +} + +.dimension-picker__item { + padding: 4px 12px; + transition: background-color 0.15s ease; +} + +.dimension-picker__item:hover, +.dimension-picker__item--hovered { + background-color: #f8f6fb; +} + +.dimension-picker__item .ant-checkbox-wrapper { + width: 100%; +} + +.dimension-picker__item .ant-checkbox .ant-checkbox-inner { + border-color: #babbb5; + background-color: #fff; +} + +.dimension-picker__item .ant-checkbox-checked .ant-checkbox-inner { + background-color: #C20075; + border-color: #C20075; +} + +.dimension-picker__item .ant-checkbox:hover .ant-checkbox-inner, +.dimension-picker__item .ant-checkbox-wrapper:hover .ant-checkbox-inner { + border-color: #C20075; +} + +.dimension-picker__item .ant-checkbox-checked:hover .ant-checkbox-inner { + background-color: #D6008A; + border-color: #D6008A; +} + +.dimension-picker__item .ant-checkbox-wrapper:not(.ant-checkbox-wrapper-disabled):hover .ant-checkbox-checked:not(.ant-checkbox-disabled) .ant-checkbox-inner { + background-color: #D6008A; + border-color: #D6008A; +} + +.dimension-picker__loading, +.dimension-picker__empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 40px 20px; +} + +/* Description Panel */ +.dimension-picker__description { + width: 260px; + padding: 10px 12px; + background-color: #fff; + flex-shrink: 0; +} + +.dimension-picker__description-title { + display: block; + margin-bottom: 8px; +} + +.dimension-picker__description-content { + margin-bottom: 0; +} + +/* Footer */ +.dimension-picker__footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-top: 1px solid #eceae9; + background-color: #fff; + border-radius: 0 0 8px 8px; +} + +.dimension-picker__footer .ant-btn-text { + color: rgb(0 0 0 / 75%); +} + +.dimension-picker__footer .ant-btn-text:hover:not(:disabled) { + color: rgb(0 0 0 / 95%); + background-color: rgb(0 0 0 / 4%); +} + +.dimension-picker__footer .ant-btn-primary { + background-color: #C20075; + border-color: #C20075; +} + +.dimension-picker__footer .ant-btn-primary:hover:not(:disabled) { + background-color: #D6008A; + border-color: #D6008A; +} + +/* Scrollbar styling */ +.dimension-picker__categories-list::-webkit-scrollbar, +.dimension-picker__items::-webkit-scrollbar { + width: 6px; +} + +.dimension-picker__categories-list::-webkit-scrollbar-track, +.dimension-picker__items::-webkit-scrollbar-track { + background: transparent; +} + +.dimension-picker__categories-list::-webkit-scrollbar-thumb, +.dimension-picker__items::-webkit-scrollbar-thumb { + background-color: #dcdcd8; + border-radius: 3px; +} + +.dimension-picker__categories-list::-webkit-scrollbar-thumb:hover, +.dimension-picker__items::-webkit-scrollbar-thumb:hover { + background-color: #babbb5; +} + diff --git a/src/components/index.ts b/src/components/index.ts index 7a32876fa6..939e00f58b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -18,6 +18,12 @@ export { Radio, type IRadioProps } from './data-entry/Radio/Radio' export { ColorPicker, type IColorPickerProps } from './not-prod-ready/ColorPicker/ColorPicker' export { Slider, type ISliderProps } from './not-prod-ready/Slider/Slider' export { Cascader, type ICascaderProps } from './data-entry/Cascader/Cascader' +export { + DimensionPicker, + type IDimensionPickerProps, + type IDimensionCategory, + type IDimensionItem, +} from './data-entry/DimensionPicker/DimensionPicker' export { DatePicker, type IDatePickerProps } from './data-entry/DatePicker/DatePicker' export { Checkbox, type ICheckboxProps } from './data-entry/Checkbox/Checkbox' export { Input, type IInputProps, type InputRef } from './data-entry/Input/Input' From af6d735f9b0a6941a0e55d52298b741f23d6ac6e Mon Sep 17 00:00:00 2001 From: astavitskaya Date: Wed, 17 Dec 2025 00:24:44 -0500 Subject: [PATCH 08/15] chore: clean up --- .../ManageColumnsDrawer.stories.tsx | 32 +--------- .../ManageColumnsDrawer.tsx | 63 ++++++------------- .../ImageCard/ImageCard.stories.tsx | 26 +------- .../data-display/ImageCard/ImageCard.tsx | 3 +- .../DimensionPicker.stories.tsx | 32 +--------- .../DimensionPicker/DimensionPicker.tsx | 14 ++--- .../DimensionPicker/dimension-picker.css | 16 +++-- 7 files changed, 38 insertions(+), 148 deletions(-) diff --git a/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.stories.tsx b/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.stories.tsx index 11d7201519..00c5938adb 100644 --- a/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.stories.tsx +++ b/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.stories.tsx @@ -4,7 +4,7 @@ import { Button, message } from 'src/components' import { ManageColumnsDrawer, type IColumnItem } from './ManageColumnsDrawer' const meta: Meta = { - title: 'UX Patterns/ManageColumnsDrawer', + title: 'POC/ManageColumnsDrawer', component: ManageColumnsDrawer, parameters: { layout: 'fullscreen', @@ -52,7 +52,7 @@ const DefaultTemplate = () => { return (
- @@ -73,31 +73,3 @@ const DefaultTemplate = () => { export const Default: Story = { render: () => , } - -const ReorderOnlyTemplate = () => { - const [open, setOpen] = useState(false) - const [dimensions, setDimensions] = useState(defaultDimensions) - const [metrics, setMetrics] = useState(defaultMetrics) - - return ( -
- - - setOpen(false)} - dimensions={dimensions} - metrics={metrics} - onDimensionsChange={setDimensions} - onMetricsChange={setMetrics} - description="Drag to reorder columns." - /> -
- ) -} - -export const ReorderOnly: Story = { - render: () => , -} diff --git a/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.tsx b/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.tsx index 280b8a16d2..61fb15f80a 100644 --- a/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.tsx +++ b/src/components/UXPatterns/ManageColumnsDrawer/ManageColumnsDrawer.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react' import { Drawer, Icon, MoreActionsButton, type IDrawerProps } from 'src/components' import { Typography } from 'src/components/general/Typography/Typography' import './ManageColumnsDrawer.css' +import { MarginXs, Padding, PaddingLg } from 'src/styles/style' export interface IColumnItem { /** @@ -98,36 +99,20 @@ const ColumnRow: React.FC = ({ }) => { const menuItems = { items: [ - ...(onRename - ? [ - { - key: 'rename', - label: ( - - - Rename - - ), - onClick: onRename, - }, - ] - : []), - ...(onRemove - ? [ - { - key: 'remove', - className: 'manage-columns-menu-item--remove', - label: ( - - - Remove - - ), - onClick: onRemove, - }, - ] - : []), - ], + onRename && { + key: 'rename', + icon: , + label: Rename, + onClick: onRename, + }, + onRemove && { + key: 'remove', + className: 'manage-columns-menu-item--remove', + icon: , + label: Remove, + onClick: onRemove, + }, + ].filter((item): item is NonNullable => Boolean(item)), } const showMoreActions = onRename ?? onRemove @@ -148,11 +133,7 @@ const ColumnRow: React.FC = ({
{item.label}
- {showMoreActions && ( -
- -
- )} + {showMoreActions && }
) } @@ -281,20 +262,16 @@ export const ManageColumnsDrawer: React.FC = ({ return ( - {title} - - } + title={{title}} open={open} onClose={onClose} width={width} className="manage-columns-drawer" {...drawerProps}>
-
- {description} -
+ + {description} + {dimensions.length > 0 && renderSection(dimensions, 'dimension', dimensionsLabel)} {metrics.length > 0 && renderSection(metrics, 'metric', metricsLabel)} diff --git a/src/components/data-display/ImageCard/ImageCard.stories.tsx b/src/components/data-display/ImageCard/ImageCard.stories.tsx index 7f0a70b84a..9013753f0c 100644 --- a/src/components/data-display/ImageCard/ImageCard.stories.tsx +++ b/src/components/data-display/ImageCard/ImageCard.stories.tsx @@ -5,7 +5,7 @@ import { Space } from 'src/components' import { ExampleStory } from 'src/utils/ExampleStory' const meta: Meta = { - title: 'Components/Data Display/ImageCard', + title: 'POC/ImageCard', component: ImageCard, args: { src: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', @@ -33,29 +33,6 @@ export default meta type Story = StoryObj -export const Primary: Story = {} - -export const ControlledSelection: Story = { - render: () => { - const [selected, setSelected] = useState(false) - - return ( - - -
Selected: {selected ? 'Yes' : 'No'}
-
- ) - }, -} - export const MultipleCards: Story = { render: () => { const [selectedIds, setSelectedIds] = useState([]) @@ -131,7 +108,6 @@ export const MultipleCards: Story = { /> ))} -
Selected: {selectedIds.length > 0 ? selectedIds.join(', ') : 'None'}
) }, diff --git a/src/components/data-display/ImageCard/ImageCard.tsx b/src/components/data-display/ImageCard/ImageCard.tsx index 96310f9074..0430dde9f8 100644 --- a/src/components/data-display/ImageCard/ImageCard.tsx +++ b/src/components/data-display/ImageCard/ImageCard.tsx @@ -150,15 +150,14 @@ export const ImageCard = (props: IImageCardProps): React.JSX.Element => { {title} {description} diff --git a/src/components/data-entry/DimensionPicker/DimensionPicker.stories.tsx b/src/components/data-entry/DimensionPicker/DimensionPicker.stories.tsx index 871171cc55..1d10c4ce91 100644 --- a/src/components/data-entry/DimensionPicker/DimensionPicker.stories.tsx +++ b/src/components/data-entry/DimensionPicker/DimensionPicker.stories.tsx @@ -79,7 +79,7 @@ const sampleItems: IDimensionItem[] = [ ] const meta: Meta = { - title: 'Components/Data Entry/DimensionPicker', + title: 'POC/DimensionPicker', component: DimensionPicker, parameters: { layout: 'centered', @@ -95,36 +95,6 @@ export default meta type Story = StoryObj export const Primary: Story = { - args: { - defaultValue: ['page-3', 'page-7'], - }, - render: args => { - const [selected, setSelected] = useState(args.defaultValue ?? []) - return ( - alert(`Applied ${keys.length} dimensions`)} - /> - ) - }, -} - -export const WithoutDescriptionPanel: Story = { - args: { - showDescriptionPanel: false, - }, -} - -export const WithoutFooter: Story = { - args: { - showApplyButton: false, - showClearAllButton: false, - }, -} - -export const Controlled: Story = { render: () => { const [selected, setSelected] = useState(['campaign-1']) return ( diff --git a/src/components/data-entry/DimensionPicker/DimensionPicker.tsx b/src/components/data-entry/DimensionPicker/DimensionPicker.tsx index 17df880e79..18bdf99235 100644 --- a/src/components/data-entry/DimensionPicker/DimensionPicker.tsx +++ b/src/components/data-entry/DimensionPicker/DimensionPicker.tsx @@ -150,7 +150,7 @@ export const DimensionPicker = ({
{/* Categories Panel */}
- + {categoryTitle}
    @@ -161,10 +161,8 @@ export const DimensionPicker = ({ key={category.key} className={`dimension-picker__category-item ${isSelected ? 'dimension-picker__category-item--selected' : ''}`} onClick={() => setSelectedCategory(category.key)}> - {category.icon && } - - {category.label} - + {category.icon && } + {category.label} ) })} @@ -196,7 +194,7 @@ export const DimensionPicker = ({ checked={isChecked} disabled={item.disabled} onChange={() => handleItemToggle(item.key)}> - {item.label} + {item.label} ) @@ -208,10 +206,10 @@ export const DimensionPicker = ({ {/* Description Panel */} {showDescriptionPanel && (
    - + {descriptionTitle} - + {hoveredItem?.description ?? 'Hover over a dimension to see its description.'}
    diff --git a/src/components/data-entry/DimensionPicker/dimension-picker.css b/src/components/data-entry/DimensionPicker/dimension-picker.css index 4816dec6a7..ec9ff6ec03 100644 --- a/src/components/data-entry/DimensionPicker/dimension-picker.css +++ b/src/components/data-entry/DimensionPicker/dimension-picker.css @@ -59,19 +59,17 @@ display: flex; align-items: center; gap: 8px; - padding: 6px 12px; + padding: 8px 12px; cursor: pointer; - transition: background-color 0.2s ease, border-left-color 0.2s ease; - border-left: 3px solid transparent; + transition: background-color 0.2s ease; } .dimension-picker__category-item:hover { - background-color: #f5f5f5; + background-color: #FAFAFA; } .dimension-picker__category-item--selected { - background-color: #f0f0f0; - border-left-color: #717368; + background-color: #FAFAFA; } .dimension-picker__category-label { @@ -96,13 +94,13 @@ } .dimension-picker__item { - padding: 4px 12px; + padding: 8px 12px; transition: background-color 0.15s ease; } .dimension-picker__item:hover, .dimension-picker__item--hovered { - background-color: #f8f6fb; + background-color: #FAFAFA; } .dimension-picker__item .ant-checkbox-wrapper { @@ -153,7 +151,7 @@ .dimension-picker__description-title { display: block; - margin-bottom: 8px; + margin-bottom: 12px; } .dimension-picker__description-content { From e5f62fa33554b88925232459f0c72b58ff0060df Mon Sep 17 00:00:00 2001 From: astavitskaya Date: Wed, 17 Dec 2025 00:53:13 -0500 Subject: [PATCH 09/15] chore: toast with actions --- .../Message/MessageNotifications.stories.tsx | 84 +++++++++++++++++++ .../Message/message-notifications.css | 29 +++++++ 2 files changed, 113 insertions(+) create mode 100644 src/components/feedback/Message/MessageNotifications.stories.tsx create mode 100644 src/components/feedback/Message/message-notifications.css diff --git a/src/components/feedback/Message/MessageNotifications.stories.tsx b/src/components/feedback/Message/MessageNotifications.stories.tsx new file mode 100644 index 0000000000..aa19f85019 --- /dev/null +++ b/src/components/feedback/Message/MessageNotifications.stories.tsx @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Message } from 'src/components/feedback/Message/Message' +import { Typography } from 'src/components/general/Typography/Typography' +import { Button } from 'src/components/general/Button/Button' +import { Flex } from 'src/components/layout/Flex/Flex' +import { ColorError, SizeXs, ColorWarning, ColorSuccess, MarginXxs, ColorTextSecondary } from 'src/styles/style' +import './message-notifications.css' + +const meta: Meta = { + title: 'POC/Message Notifications', + component: Message, +} + +export default meta + +type Story = StoryObj + +export const AllStates: Story = { + render: () => { + const showAllMessages = () => { + // Error + void Message.error({ + content: ( + + This project has been unpublished + + Removing all users has unpublished this project. Add users to republish. + + + + ), + duration: 0, + className: 'message-notification-custom message-notification-error', + }) + + // Warning + setTimeout(() => { + void Message.warning({ + content: ( + + This project has been unpublished + + Removing all users has unpublished this project. Add users to republish. + + + + ), + duration: 0, + className: 'message-notification-custom message-notification-warning', + }) + }, 100) + + // Success + setTimeout(() => { + void Message.success({ + content: ( + + Successfully updated profile + + Your changes have been saved and your profile is live. Your team can make edits. + + + + + + + ), + duration: 0, + className: 'message-notification-custom message-notification-success', + }) + }, 200) + } + + return + }, +} diff --git a/src/components/feedback/Message/message-notifications.css b/src/components/feedback/Message/message-notifications.css new file mode 100644 index 0000000000..2ea90b2a87 --- /dev/null +++ b/src/components/feedback/Message/message-notifications.css @@ -0,0 +1,29 @@ +/* Custom styling for message notifications */ +.ant-message .ant-message-notice-wrapper .message-notification-custom.ant-message-notice .ant-message-notice-content { + text-align: left; +} + +/* Set flex start on custom content child */ +.ant-message .ant-message-notice-wrapper .message-notification-custom.ant-message-notice .ant-message-notice-content > .ant-message-custom-content { + display: flex; + align-items: flex-start !important; +} + +/* Move icons down by 4px */ +.ant-message .ant-message-notice-wrapper .message-notification-custom.ant-message-notice .ant-message-custom-content .anticon { + margin-top: 4px; +} + +/* Background colors for different message types */ +.ant-message .ant-message-notice-wrapper .message-notification-error.ant-message-notice .ant-message-notice-content { + background-color: var(--color-error-bg); +} + +.ant-message .ant-message-notice-wrapper .message-notification-warning.ant-message-notice .ant-message-notice-content { + background-color: var(--color-warning-bg); +} + +.ant-message .ant-message-notice-wrapper .message-notification-success.ant-message-notice .ant-message-notice-content { + background-color: var(--color-success-bg); +} + From 2220e82cf7b20304f271b2b268a3337c26b7b341 Mon Sep 17 00:00:00 2001 From: skosemP Date: Thu, 18 Dec 2025 11:28:30 -0500 Subject: [PATCH 10/15] chore: refresh badge docs (#695) Co-authored-by: Anastasiia Stavitskaya --- .../Data Display/Badge/Documentation.mdx | 14 +++--- .../data-display/Badge/Badge.stories.tsx | 46 ++++++++++--------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/components/Data Display/Badge/Documentation.mdx b/docs/components/Data Display/Badge/Documentation.mdx index 96aa428d1d..4146bb68e2 100644 --- a/docs/components/Data Display/Badge/Documentation.mdx +++ b/docs/components/Data Display/Badge/Documentation.mdx @@ -12,23 +12,21 @@ import * as BadgeStories from '../../../../src/components/data-display/Badge/Bad The **Badge** component provides visual indicators for an item’s status, available in two styles: a **dot** and a \*\*status badge" with text. -### [Dot Badge](https://mparticle.github.io/aquarium/?path=/story/components-data-display-badge--dot-badge) +#### Status badge -#### When to use +Use status badges to indicate where an item sits in a process with a clear beginning and end. Pair the dot with concise labels so the current state is obvious without extra context. -Use **Badge** when a status needs to be displayed next to an element in a compact form. + -#### Current Usages +#### Dot badge -- **[Navigation](https://mparticle.github.io/aquarium/?path=/story/components-navigation-globalnavigation--primary)** – Badge is used within the navigation sidebar for notification use cases. +Use dot badges to surface the current status of an item without additional copy. Each dot maps to the core status tokens, including the paused state for work that is temporarily on hold. -#### Related Links +#### Related links | Type | Resource | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Eames | [Badge Component](https://www.figma.com/design/veXnmignQnJz8StIq10VJ5/Eames-2.0---Foundations-%26-Components?node-id=399-0&node-type=canvas&t=B6HJWqqDsUOypZQj-0) | | AntD | [Badge Component](https://ant.design/components/badge) | - - diff --git a/src/components/data-display/Badge/Badge.stories.tsx b/src/components/data-display/Badge/Badge.stories.tsx index 2016f743fb..4446b6fcac 100644 --- a/src/components/data-display/Badge/Badge.stories.tsx +++ b/src/components/data-display/Badge/Badge.stories.tsx @@ -1,8 +1,7 @@ import { type Meta, type StoryObj } from '@storybook/react' import { Badge } from 'src/components/data-display/Badge/Badge' -import { ColorPrimary } from 'src/styles/style' -import { ExampleStory } from 'src/utils/ExampleStory' import { Space } from 'src/components' +import { ColorPrimary, ColorSuccess, ColorWarning, ColorError, ColorBorderSecondary } from 'src/styles/style' const meta: Meta = { title: 'Components/Data Display/Badge', @@ -18,9 +17,22 @@ type Story = StoryObj */ export const DotBadge: Story = { - args: { - dot: true, - color: ColorPrimary, + render: () => { + const colors = [ + { label: 'Primary', value: ColorPrimary }, + { label: 'Success', value: ColorSuccess }, + { label: 'Warning', value: ColorWarning }, + { label: 'Error', value: ColorError }, + { label: 'Paused', value: ColorBorderSecondary }, + ] as const + + return ( + + {colors.map(({ label, value }) => ( + + ))} + + ) }, } @@ -33,23 +45,13 @@ export const StatusBadge: Story = { }, render: () => { return ( - - - - - - - - -
    - - - - - - - -
    + + + + + + + ) }, } From c04d147f3e747c4d35294e03d39dcd2485741a79 Mon Sep 17 00:00:00 2001 From: skosemP Date: Thu, 18 Dec 2025 13:36:16 -0500 Subject: [PATCH 11/15] chore: segmented size docs (#708) --- .../Data Display/Segmented/Documentation.mdx | 24 +++++++++++++---- .../Segmented/Segmented.stories.tsx | 27 +++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/docs/components/Data Display/Segmented/Documentation.mdx b/docs/components/Data Display/Segmented/Documentation.mdx index 2ab4a2ff64..9604bbbae3 100644 --- a/docs/components/Data Display/Segmented/Documentation.mdx +++ b/docs/components/Data Display/Segmented/Documentation.mdx @@ -4,12 +4,26 @@ import * as SegmentedStories from '../../../../src/components/data-display/Segme -{/* Documentation goes here */} - # Segmented -This is the documentation for the Segmenteds component +#### Overview + +The **Segmented** component presents a compact set of mutually exclusive choices. Each option is easy to scan and the selected value is highlighted to confirm the current state. + +#### Small segmented control + +Use the small size when the control sits inside dense layouts such as tables or cards with limited space. + + + +#### Large segmented control + +Use the large size for standalone toggles where the segment labels need more breathing room and legibility. + + -{/* Documentation goes here */} +#### Related links - +| Type | Resource | +| ---- | -------- | +| AntD | [Segmented Component](https://ant.design/components/segmented) | diff --git a/src/components/data-display/Segmented/Segmented.stories.tsx b/src/components/data-display/Segmented/Segmented.stories.tsx index ec86d385f1..223944b641 100644 --- a/src/components/data-display/Segmented/Segmented.stories.tsx +++ b/src/components/data-display/Segmented/Segmented.stories.tsx @@ -1,14 +1,37 @@ import { type Meta, type StoryObj } from '@storybook/react' import { Segmented } from 'src/components/data-display/Segmented/Segmented' +const segmentedOptions = [ + { label: 'Daily', value: 'daily' }, + { label: 'Weekly', value: 'weekly' }, + { label: 'Monthly', value: 'monthly' }, +] + const meta: Meta = { title: 'Components/Not Prod Ready/Data Display/Segmented', component: Segmented, - args: {}, + args: { + options: segmentedOptions, + defaultValue: segmentedOptions[0]?.value, + size: 'middle', + }, + argTypes: { + onChange: { action: 'change' }, + }, } export default meta type Story = StoryObj -export const Primary: Story = {} +export const Small: Story = { + args: { + size: 'small', + }, +} + +export const Large: Story = { + args: { + size: 'large', + }, +} From a681d0bfae9b819a68b9b0ee43eeca184b20f9c0 Mon Sep 17 00:00:00 2001 From: Anastasiia Stavitskaya Date: Thu, 18 Dec 2025 16:00:29 -0500 Subject: [PATCH 12/15] feat: eoy components clean up (#703) --- .../Data Display/Popover/Documentation.mdx | 4 +- .../StatisticsCard/Documentation.mdx | 23 ++++++++ .../UX Patterns/Steps/Documentation.mdx | 12 ++++ .../StatisticsCard/StatisticsCard.stories.tsx | 57 +++++++++++++++++++ .../StatisticsCard/StatisticsCard.tsx | 42 ++++++++++++++ .../UXPatterns/Steps/Steps.stories.tsx | 41 +++++++++++++ .../data-display/Popover/Popover.stories.tsx | 32 +++++++++++ .../data-display/Tooltip/IconWithTooltip.tsx | 28 +++++++++ .../data-display/Tooltip/Tooltip.stories.tsx | 17 ++++-- .../general/Button/Button.stories.tsx | 31 +++++++++- src/components/general/Button/Button.tsx | 16 +++++- src/components/general/Typography/colors.ts | 9 ++- src/components/index.ts | 2 + 13 files changed, 301 insertions(+), 13 deletions(-) create mode 100644 docs/components/UX Patterns/StatisticsCard/Documentation.mdx create mode 100644 docs/components/UX Patterns/Steps/Documentation.mdx create mode 100644 src/components/UXPatterns/StatisticsCard/StatisticsCard.stories.tsx create mode 100644 src/components/UXPatterns/StatisticsCard/StatisticsCard.tsx create mode 100644 src/components/UXPatterns/Steps/Steps.stories.tsx create mode 100644 src/components/data-display/Tooltip/IconWithTooltip.tsx diff --git a/docs/components/Data Display/Popover/Documentation.mdx b/docs/components/Data Display/Popover/Documentation.mdx index 24c9d6194f..750ae96235 100644 --- a/docs/components/Data Display/Popover/Documentation.mdx +++ b/docs/components/Data Display/Popover/Documentation.mdx @@ -1,6 +1,6 @@ import { Meta, Canvas } from '@storybook/blocks' -import * as PopoverStories from '../../../../src/components/data-display/Popover/Popover.stories' +import PopoverStories, { Primary as PopoverPrimary } from '../../../../src/components/data-display/Popover/Popover.stories' @@ -34,4 +34,4 @@ Use [Tooltip](https://mparticle.github.io/aquarium/?path=/story/components-data- | AntD | [Popover Component](https://ant.design/components/popover) | - + diff --git a/docs/components/UX Patterns/StatisticsCard/Documentation.mdx b/docs/components/UX Patterns/StatisticsCard/Documentation.mdx new file mode 100644 index 0000000000..79023390dd --- /dev/null +++ b/docs/components/UX Patterns/StatisticsCard/Documentation.mdx @@ -0,0 +1,23 @@ +import { Meta, Canvas } from '@storybook/blocks' + +import StatisticsCardStories, { ModelMetrics } from '../../../../src/components/UXPatterns/StatisticsCard/StatisticsCard.stories' + + + +# Statistics Card + +A card component for displaying metric values with optional tooltips, commonly used for model metrics, performance indicators, or key statistics. + +## When to Use + +- Displaying model performance metrics (accuracy, precision, recall, AUC) +- Showing key performance indicators (KPIs) +- Presenting statistical data with contextual help +- Creating metric dashboards or summary cards + +## Model Metrics Example + +A common pattern showing multiple metrics in a grid layout: + + + diff --git a/docs/components/UX Patterns/Steps/Documentation.mdx b/docs/components/UX Patterns/Steps/Documentation.mdx new file mode 100644 index 0000000000..40a868b01c --- /dev/null +++ b/docs/components/UX Patterns/Steps/Documentation.mdx @@ -0,0 +1,12 @@ +import { Meta, Canvas } from '@storybook/blocks' + +import StepsStories, { UXPatternExample } from '../../../../src/components/UXPatterns/Steps/Steps.stories' + + + +# Steps Progress Indicator + +Steps help communicate the current state of multistep flows. The following UX pattern centers the vertical label placement version inside its container so it feels like a dashboard callout. + + + diff --git a/src/components/UXPatterns/StatisticsCard/StatisticsCard.stories.tsx b/src/components/UXPatterns/StatisticsCard/StatisticsCard.stories.tsx new file mode 100644 index 0000000000..dc9373d721 --- /dev/null +++ b/src/components/UXPatterns/StatisticsCard/StatisticsCard.stories.tsx @@ -0,0 +1,57 @@ +import { type Meta, type StoryObj } from '@storybook/react' +import { StatisticsCard } from './StatisticsCard' +import { Row, Col } from 'src/components' + +const meta: Meta = { + title: 'UX Patterns/StatisticsCard', + component: StatisticsCard, + parameters: { + layout: 'centered', + }, +} +export default meta + +type Story = StoryObj + +export const ModelMetrics: Story = { + render: () => { + const metrics = [ + { + title: 'Precision', + value: 72, + tooltip: + 'Precision focuses on how many of the users predicted to act actually do so. High precision means less wasted impressions or messaging.', + }, + { + title: 'Recall', + value: 76, + tooltip: + 'Recall shows how many of the actual converters the model successfully identifies. Higher recall means fewer missed opportunities.', + }, + { + title: 'Accuracy', + value: 87, + tooltip: + "Accuracy shows how often the model's predictions match what really happens, giving you confidence in your audience targeting or personalization strategy.", + }, + { + title: 'AUC', + value: 87, + tooltip: + 'AUC tells you how powerful your model is at separating likely converters from everyone else — the higher the AUC, the better your targeting precision.', + }, + ] + + return ( +
    + + {metrics.map(metric => ( + + + + ))} + +
    + ) + }, +} diff --git a/src/components/UXPatterns/StatisticsCard/StatisticsCard.tsx b/src/components/UXPatterns/StatisticsCard/StatisticsCard.tsx new file mode 100644 index 0000000000..a458c89c67 --- /dev/null +++ b/src/components/UXPatterns/StatisticsCard/StatisticsCard.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { Card, Flex, Statistic, IconWithTooltip } from 'src/components' +import type { ReactNode } from 'react' +import { FontSizeHeading3, FontSizeLg, FontWeightStrong } from 'src/styles/style' + +export interface IStatisticsCardProps { + title: string + value: string | number + denominator?: string | number + tooltip?: ReactNode + valueFontSize?: string + denominatorFontSize?: string + valueFontWeight?: string | number +} + +export const StatisticsCard: React.FC = ({ + title, + value, + denominator = '100', + tooltip, + valueFontSize = FontSizeHeading3, + denominatorFontSize = FontSizeLg, + valueFontWeight = FontWeightStrong, +}) => { + return ( + + + ( + <> + {value} + / {denominator} + + )} + /> + {tooltip && } + + + ) +} diff --git a/src/components/UXPatterns/Steps/Steps.stories.tsx b/src/components/UXPatterns/Steps/Steps.stories.tsx new file mode 100644 index 0000000000..82b9708663 --- /dev/null +++ b/src/components/UXPatterns/Steps/Steps.stories.tsx @@ -0,0 +1,41 @@ +import { type Meta, type StoryObj } from '@storybook/react' +import { Center } from 'src/components' +import { Steps } from 'src/components/navigation/Steps/Steps' + +const items = [ + { + title: 'Define request', + description: 'Gather requirements', + }, + { + title: 'Review', + description: 'Validate with stakeholders', + }, + { + title: 'Approve', + description: 'Get program sign-off', + }, +] + +const meta: Meta = { + title: 'UX Patterns/Steps', + component: Steps, + parameters: { + layout: 'centered', + }, +} +export default meta + +type Story = StoryObj + +export const UXPatternExample: Story = { + render: () => { + const currentStep = 3 + + return ( +
    + +
    + ) + }, +} diff --git a/src/components/data-display/Popover/Popover.stories.tsx b/src/components/data-display/Popover/Popover.stories.tsx index e32b580643..60dda19216 100644 --- a/src/components/data-display/Popover/Popover.stories.tsx +++ b/src/components/data-display/Popover/Popover.stories.tsx @@ -1,6 +1,9 @@ import { Popover, type IPopoverProps } from 'src/components/data-display/Popover/Popover' import { type Meta, type StoryObj } from '@storybook/react' import { Button } from 'src/components/general/Button/Button' +import { Tag } from 'src/components/data-display/Tag/Tag' +import { Typography } from 'src/components/general/Typography/Typography' +import { Flex } from 'src/components/layout/Flex/Flex' const meta: Meta = { title: 'Components/Data Display/Popover', @@ -38,3 +41,32 @@ export const Primary: Story = { }, render: PrimaryTemplate, } + +const PopoverWithLinksTemplate = (args: IPopoverProps) => { + const content = ( + + Link Example 1 + Link Example 2 + Link Example 3 + + ) + + return ( + <> + + {args.children ?? '3'} + + + ) +} + +export const ExampleWithLinks: Story = { + args: { trigger: 'click', children: '3' }, + argTypes: { + trigger: { + control: 'select', + options: ['click', 'hover', 'focus'], + }, + }, + render: PopoverWithLinksTemplate, +} diff --git a/src/components/data-display/Tooltip/IconWithTooltip.tsx b/src/components/data-display/Tooltip/IconWithTooltip.tsx new file mode 100644 index 0000000000..b14baba56e --- /dev/null +++ b/src/components/data-display/Tooltip/IconWithTooltip.tsx @@ -0,0 +1,28 @@ +import { Flex } from 'src/components/layout/Flex/Flex' +import { Icon, type IIconProps } from 'src/components/general/Icon/Icon' +import { Tooltip, type ITooltipProps } from './Tooltip' +import type { IconNames } from 'src/types/icons' +import type { ReactNode } from 'react' + +export interface IIconWithTooltipProps extends Omit { + title: ReactNode + iconName?: IconNames + iconProps?: Omit +} + +export const IconWithTooltip = ({ title, iconName = 'help', iconProps, ...rest }: IIconWithTooltipProps) => { + const iconPropsWithDefaults: IIconProps = { + name: iconName, + size: 'sm', + color: 'default', + ...iconProps, + } + + return ( + + + + + + ) +} diff --git a/src/components/data-display/Tooltip/Tooltip.stories.tsx b/src/components/data-display/Tooltip/Tooltip.stories.tsx index a6549c6b58..1ccc8a3945 100644 --- a/src/components/data-display/Tooltip/Tooltip.stories.tsx +++ b/src/components/data-display/Tooltip/Tooltip.stories.tsx @@ -1,6 +1,7 @@ import { type Meta, type StoryObj } from '@storybook/react' -import { Flex, Tooltip, Typography } from 'src/components' +import { Flex, Tooltip, Typography, IconWithTooltip } from 'src/components' import { Button } from 'src/components' +import { MarginXxs } from 'src/styles/style' const meta: Meta = { title: 'Components/Data Display/Tooltip', @@ -69,7 +70,13 @@ export const WithLink: Story = { }, } -/* - Initial story templates generated by AI. - Customize the stories based on specific requirements. -*/ +export const TextWithIcon: Story = { + render: () => { + return ( + + Strength + + + ) + }, +} diff --git a/src/components/general/Button/Button.stories.tsx b/src/components/general/Button/Button.stories.tsx index 726a1131bf..b4dda18a14 100644 --- a/src/components/general/Button/Button.stories.tsx +++ b/src/components/general/Button/Button.stories.tsx @@ -1,8 +1,9 @@ import { Button } from 'src/components/general/Button/Button' import { type Meta, type StoryObj } from '@storybook/react' import { userEvent } from '@storybook/test' -import { Alert, Flex, Icon, Typography, Tooltip } from 'src/components' +import { Alert, Dropdown, Flex, Icon, Typography, Tooltip } from 'src/components' import React from 'react' +import type { MenuProps } from 'antd' import { BorderRadiusLg, ColorBorderSecondary, ColorWhite, MarginMd, SizeXs } from 'src/styles/style' const meta: Meta = { @@ -267,3 +268,31 @@ export const Refresh: Story = { ) }, } + +export const With2Options: Story = { + render: () => { + const refreshMenu: MenuProps = { + items: [ + { + key: 'refresh-columns', + label: 'Refresh Columns', + }, + { + key: 'refresh-values', + label: 'Refresh Values', + }, + ], + onClick: ({ key }) => { + console.log('Selected:', key) + }, + } + + return ( + + + + ) + }, +} diff --git a/src/components/general/Button/Button.tsx b/src/components/general/Button/Button.tsx index 12bd359da7..05b7088ba9 100644 --- a/src/components/general/Button/Button.tsx +++ b/src/components/general/Button/Button.tsx @@ -7,7 +7,7 @@ import { ConfigProvider } from 'src/components/other/ConfigProvider/ConfigProvid import { type ReactNode } from 'react' import './button.css' -export interface IButtonProps extends Omit { +export interface IButtonProps extends Omit { /** * @deprecated This variant is a temporary fix for new icons. * Use this variant only with new icons to align the icon and text centered. @@ -15,6 +15,10 @@ export interface IButtonProps extends Omit { */ variant?: 'with-new-icon' icon?: ReactNode + /** + * Color of the button text. Use 'inherit' to inherit from parent element. + */ + color?: 'inherit' | string } export const Button = (props: IButtonProps) => { const classMap = { @@ -30,11 +34,17 @@ export const Button = (props: IButtonProps) => { const extraClass = props.variant === 'with-new-icon' ? classMap['with-new-icon'] : '' - const { variant, ...restProps } = props + const { variant, color, style, ...restProps } = props + + const buttonStyle = color ? { ...style, color } : style return ( - + {props.children} diff --git a/src/components/general/Typography/colors.ts b/src/components/general/Typography/colors.ts index af807bcd0c..5ec6f2d7ed 100644 --- a/src/components/general/Typography/colors.ts +++ b/src/components/general/Typography/colors.ts @@ -28,13 +28,18 @@ export const TypographyColors = [ 'ColorTextLabel', 'ColorTextDescription', 'ColorTextLightSolid', + 'inherit', ] as const export type TypographyColor = (typeof TypographyColors)[number] export function getColorFromStyles(color: TypographyColor | string): string { - if (styles[color as TypographyColor]) { - return (styles as unknown as Record)[color as TypographyColor] + if (color === 'inherit') { + return 'inherit' + } + + if (styles[color as keyof typeof styles]) { + return (styles as unknown as Record)[color] } return color diff --git a/src/components/index.ts b/src/components/index.ts index 3019026766..a4bd6790e2 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -41,6 +41,7 @@ export { Tag, type ITagProps } from './data-display/Tag/Tag' export { Tour, type ITourProps } from './data-display/Tour/Tour' export { Carousel, type ICarouselProps } from './not-prod-ready/Carousel/Carousel' export { Tooltip, type ITooltipProps } from './data-display/Tooltip/Tooltip' +export { IconWithTooltip, type IIconWithTooltipProps } from './data-display/Tooltip/IconWithTooltip' export { Statistic, type IStatisticProps } from './not-prod-ready/Statistic/Statistic' export { Tree, type ITreeProps, type ITreeData } from './not-prod-ready/Tree/Tree' export { Image, type IImageProps } from './data-display/Image/Image' @@ -155,6 +156,7 @@ export { type IMoreActionsButtonProps, type IMoreActionsButtonItem, } from './UXPatterns/MoreActionsButton/MoreActionsButton' +export { StatisticsCard, type IStatisticsCardProps } from './UXPatterns/StatisticsCard/StatisticsCard' export { UnauthorizedTooltip, type IUnauthorizedTooltipProps, From ed502249c43d7dbda01d25bc28c9000a233ebb36 Mon Sep 17 00:00:00 2001 From: skosemP Date: Thu, 18 Dec 2025 18:19:52 -0500 Subject: [PATCH 13/15] fix: theme checkbox group (#711) Co-authored-by: astavitskaya Co-authored-by: Anastasiia Stavitskaya --- .../Data Entry/Checkbox/Documentation.mdx | 38 +++- .../data-entry/Checkbox/Checkbox.stories.tsx | 206 +++++++++++++++++- .../data-entry/Checkbox/Checkbox.tsx | 14 +- .../data-entry/Checkbox/checkbox.css | 6 + 4 files changed, 252 insertions(+), 12 deletions(-) diff --git a/docs/components/Data Entry/Checkbox/Documentation.mdx b/docs/components/Data Entry/Checkbox/Documentation.mdx index 922e5016b9..d14aeb07dd 100644 --- a/docs/components/Data Entry/Checkbox/Documentation.mdx +++ b/docs/components/Data Entry/Checkbox/Documentation.mdx @@ -19,16 +19,38 @@ Use the **Checkbox** component when: - For binary options, especially when changes require saving. - For active agreements, such as accepting terms of service. -When selecting a single option from a set of mutually exclusive choices, use **[Radio Buttons](https://mparticle.github.io/aquarium/?path=/story/components-data-entry-radio--primary)**. +### Default checkbox -Use **[switch](https://mparticle.github.io/aquarium/?path=/story/components-data-entry-switch--primary)** for cases like activation, filter controls, or subscriptions where there is a clear "on/off" state. +Start with the standard checkbox for simple agree-or-disagree prompts. -#### Related Links + -| Type | Resource | -| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Eames | [Checkbox Component](https://www.figma.com/design/veXnmignQnJz8StIq10VJ5/Eames-2.0---Foundations-%26-Components?node-id=388-11036&p=f&t=zURbLpG60aM6aJiH-0) | -| AntD | [Checkbox Component](https://ant.design/components/checkbox) | +### Checkbox states +Reference this overview to show each visual state side by side—interactive default, checked, indeterminate, and disabled. - + + +### Checkbox groups + +Group related choices with `Checkbox.Group` so selections stay in sync. Pair the group with a leading Checkbox to provide a “Select all” control when the pattern fits the use case. + + + +### Disabled with tooltip + +Pair a disabled checkbox with a tooltip so users understand why the option is locked or how to gain access. + + + +### Stacked checkbox list + +Use individual Checkbox components when you need richer labels and supporting text. Keep the layout vertically aligned to make scanning easy. + + + +### Long consent copy + +Pair a checkbox with structured rich text for legal agreements. Highlight the primary sentence and keep supporting copy visually secondary. + + diff --git a/src/components/data-entry/Checkbox/Checkbox.stories.tsx b/src/components/data-entry/Checkbox/Checkbox.stories.tsx index 9ac395cea7..600970d6e0 100644 --- a/src/components/data-entry/Checkbox/Checkbox.stories.tsx +++ b/src/components/data-entry/Checkbox/Checkbox.stories.tsx @@ -1,7 +1,8 @@ import { type Meta } from '@storybook/react' import { type StoryObj } from '@storybook/react' -import { Checkbox } from 'src/components' +import { Checkbox, Tooltip, Typography, Flex } from 'src/components' import { useState } from 'react' +import { SizeSm, SizeXs, SizeXl } from 'src/styles/style' export type CheckboxValueType = string | number | boolean @@ -37,6 +38,144 @@ export default meta type Story = StoryObj +const featureOptions = [ + { label: 'Feature A', value: 'feature-a' }, + { label: 'Feature B', value: 'feature-b' }, + { label: 'Feature C', value: 'feature-c' }, +] + +const CheckboxStatesExample = () => { + const [isChecked, setIsChecked] = useState(false) + + return ( +
    + { + setIsChecked(event.target.checked) + }}> + Default (interactive) + + Checked + Indeterminate + Disabled + + Disabled and checked + +
    + ) +} + +const CheckboxGroupExample = () => { + const [selectedOptions, setSelectedOptions] = useState(['feature-a', 'feature-c']) + + const allValues = featureOptions.map(option => option.value) + const isAllSelected = selectedOptions.length === featureOptions.length + const isIndeterminate = selectedOptions.length > 0 && !isAllSelected + + return ( +
    + { + setSelectedOptions(event.target.checked ? allValues : []) + }}> + Select all features + +
    + { + setSelectedOptions(values) + }} + /> +
    +
    + ) +} + +const preferenceOptions = [ + { + label: 'Product updates', + value: 'product-updates', + description: 'Important improvements, release notes, and early access invitations.', + }, + { + label: 'Webinars and events', + value: 'events', + description: 'Invitations to upcoming live sessions, workshops, and community meetups.', + }, + { + label: 'Security advisories', + value: 'security', + description: 'Critical notifications about security and compliance-related changes.', + }, +] + +const CheckboxListExample = () => { + const [selectedValues, setSelectedValues] = useState(['product-updates']) + + const toggleValue = (value: CheckboxValueType, shouldSelect: boolean) => { + setSelectedValues(previousValues => { + if (shouldSelect) { + return previousValues.includes(value) ? previousValues : [...previousValues, value] + } + + return previousValues.filter(existingValue => existingValue !== value) + }) + } + + return ( +
    + {preferenceOptions.map(option => { + const isChecked = selectedValues.includes(option.value) + return ( + { + toggleValue(option.value, event.target.checked) + }}> + + {option.label} + {option.description} + + + ) + })} +
    + ) +} + +const LongLegalCopyExample = () => { + const [checked, setChecked] = useState(true) + + return ( +
    + { + setChecked(event.target.checked) + }}> + + I agree to the terms, policies, and any other legal guidelines required to use this service, including matters + related to privacy, data usage, third-party tools, cookies, and future updates to the agreement. + + +
    + ) +} + +const DisabledWithTooltipExample = () => ( + + + Checkbox label + + +) + /* Initial story templates generated by AI. Customize the stories based on specific requirements. @@ -45,6 +184,69 @@ type Story = StoryObj export const Primary: Story = { args: { disabled: false, - children: 'Don’t show this message again', + children: 'Checkbox label', + }, +} + +export const StatesShowcase: Story = { + name: 'States showcase', + render: () => , + parameters: { + docs: { + description: { + story: 'Display the default, checked, indeterminate, and disabled states together for fast visual comparison.', + }, + }, + }, +} + +export const DisabledWithTooltip: Story = { + name: 'Disabled with tooltip', + render: () => , + parameters: { + docs: { + description: { + story: 'Wrap a disabled checkbox with a tooltip to explain why the option is unavailable.', + }, + }, + }, +} + +export const GroupSelection: Story = { + name: 'Group selection', + render: () => , + parameters: { + docs: { + description: { + story: + 'Use `Checkbox.Group` to render related choices. Combine it with a single Checkbox to create a “Select all” control when needed.', + }, + }, + }, +} + +export const MultipleOptions: Story = { + name: 'Multiple options', + render: () => , + parameters: { + docs: { + description: { + story: + 'Stack individual checkboxes to present detailed choices. Each checkbox can include supporting text to clarify what opting in entails.', + }, + }, + }, +} + +export const LongFormConsent: Story = { + name: 'Long form consent', + render: () => , + parameters: { + docs: { + description: { + story: + 'Use a single Checkbox with stacked typography for lengthy consent copy. Keep supporting text in a secondary color to maintain readability.', + }, + }, }, } diff --git a/src/components/data-entry/Checkbox/Checkbox.tsx b/src/components/data-entry/Checkbox/Checkbox.tsx index b1dd1ba91e..f7236efa68 100644 --- a/src/components/data-entry/Checkbox/Checkbox.tsx +++ b/src/components/data-entry/Checkbox/Checkbox.tsx @@ -1,11 +1,13 @@ import { Checkbox as AntCheckbox } from 'antd' import { type CheckboxProps as AntCheckboxProps } from 'antd' +import { type CheckboxGroupProps as AntCheckboxGroupProps } from 'antd/es/checkbox/Group' import { ConfigProvider } from 'src/components' import './checkbox.css' export interface ICheckboxProps extends AntCheckboxProps {} +export interface ICheckboxGroupProps extends AntCheckboxGroupProps {} -export const Checkbox = (props: ICheckboxProps) => { +const CheckboxComponent = (props: ICheckboxProps) => { return ( @@ -13,4 +15,12 @@ export const Checkbox = (props: ICheckboxProps) => { ) } -Checkbox.Group = AntCheckbox.Group +const CheckboxGroup = (props: ICheckboxGroupProps) => { + return ( + + + + ) +} + +export const Checkbox = Object.assign(CheckboxComponent, { Group: CheckboxGroup }) diff --git a/src/components/data-entry/Checkbox/checkbox.css b/src/components/data-entry/Checkbox/checkbox.css index ed954e67a6..7cef200a72 100644 --- a/src/components/data-entry/Checkbox/checkbox.css +++ b/src/components/data-entry/Checkbox/checkbox.css @@ -1,4 +1,10 @@ .ant-checkbox-wrapper { + display: inline-flex; + align-items: flex-start; font-weight: 400; } +.ant-checkbox { + align-self: flex-start; + margin-top: var(--margin-xxs); +} From 4aaab19a6105271f4e620b44cf5d0420741540f9 Mon Sep 17 00:00:00 2001 From: skosemP Date: Thu, 18 Dec 2025 18:34:30 -0500 Subject: [PATCH 14/15] chore: refresh avatar examples (#710) Co-authored-by: astavitskaya Co-authored-by: Anastasiia Stavitskaya --- .../Data Display/Avatar/Documentation.mdx | 31 +++-- .../data-display/Avatar/Avatar.stories.tsx | 111 +++++++++++++++++- src/components/data-display/Avatar/Avatar.tsx | 1 + src/components/data-display/Avatar/avatar.css | 9 ++ 4 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 src/components/data-display/Avatar/avatar.css diff --git a/docs/components/Data Display/Avatar/Documentation.mdx b/docs/components/Data Display/Avatar/Documentation.mdx index 77bed11116..afd062b9ef 100644 --- a/docs/components/Data Display/Avatar/Documentation.mdx +++ b/docs/components/Data Display/Avatar/Documentation.mdx @@ -6,18 +6,31 @@ import * as AvatarStories from '../../../../src/components/data-display/Avatar/A # Avatar -#### Overview +#### Shape -The **Avatar** component is used to display a profile image, initials, or an icon, commonly for identification purposes. +The default avatar is circular. Switch the `shape` prop to `square` when you need hard edges or when aligning with adjacent rectangular content. -#### When to use + -- To represent an account, organization, or workspace. -- To represent a single user or as a placeholder when a user profile image isn’t available. +#### Sizes -#### Current Usage + -- **[Navigation](https://mparticle.github.io/aquarium/?path=/story/components-navigation-globalnavigation--primary)** – Avatar is used within the navigation sidebar to represent the selected workspace. +#### Photo avatar + + + +#### Icon avatar + + + +#### Initial avatar + + + +#### Badge avatars + + #### Related Links @@ -26,7 +39,3 @@ The **Avatar** component is used to display a profile image, initials, or an ico | Eames | [Avatar Component](https://www.figma.com/design/veXnmignQnJz8StIq10VJ5/Eames-2.0---Foundations-%26-Components?node-id=397-12044&node-type=canvas&t=B6HJWqqDsUOypZQj-0) | | AntD | [Avatar Component](https://ant.design/components/avatar) | - - - - diff --git a/src/components/data-display/Avatar/Avatar.stories.tsx b/src/components/data-display/Avatar/Avatar.stories.tsx index 634b377842..f7621364e6 100644 --- a/src/components/data-display/Avatar/Avatar.stories.tsx +++ b/src/components/data-display/Avatar/Avatar.stories.tsx @@ -1,5 +1,10 @@ import { type Meta, type StoryObj } from '@storybook/react' -import { Avatar, Icon } from 'src/components' +import { Avatar, Badge, Icon, Space, type IIconProps } from 'src/components' +import { MpBrandSecondary3, MpBrandSecondary4, MpBrandSecondary8, Size } from 'src/styles/style' + +const neutralAvatarStyle = { backgroundColor: MpBrandSecondary4, color: MpBrandSecondary8 } +const neutralIconProps: IIconProps = { name: 'userProfiles', size: 'lg' } +const spaceSize = parseInt(Size, 10) const meta: Meta = { title: 'Components/Data Display/Avatar', @@ -14,14 +19,110 @@ type Story = StoryObj Customize the stories based on specific requirements. */ -export const WithInitials: Story = { +export const ShapeExamples: Story = { + render: () => { + return ( + + MP + + MP + + + ) + }, +} + +export const SizeExamples: Story = { + render: () => { + return ( + + + MP + + MP + + MP + + + ) + }, +} + +export const TypePhoto: Story = { args: { - children: 'mP', + src: 'https://randomuser.me/api/portraits/women/72.jpg', + alt: 'Avatar with photo', }, } -export const WithIcon: Story = { +export const TypeIcon: Story = { args: { - icon: , + style: neutralAvatarStyle, + icon: , + }, +} + +export const TypeInitials: Story = { + args: { + children: 'MP', + }, +} + +export const WithBadge: Story = { + render: () => { + return ( + + + } /> + + + MP + + + ) + }, +} + +export const WithGroup: Story = { + render: () => { + const teammates = [ + { + name: 'Chloe Martinez', + src: 'https://randomuser.me/api/portraits/women/65.jpg', + }, + { + name: 'Ernest Wallace', + src: 'https://randomuser.me/api/portraits/men/34.jpg', + }, + { + name: 'Priya Patel', + initials: 'PP', + }, + { + name: 'Lars Ostergaard', + initials: 'LO', + }, + ] + + return ( + + + {teammates.map(teammate => { + const style = teammate.initials != null ? neutralAvatarStyle : undefined + + return ( + + {teammate.initials} + + ) + })} + + + ) }, } diff --git a/src/components/data-display/Avatar/Avatar.tsx b/src/components/data-display/Avatar/Avatar.tsx index d8bb0eb44f..63ff106166 100644 --- a/src/components/data-display/Avatar/Avatar.tsx +++ b/src/components/data-display/Avatar/Avatar.tsx @@ -1,6 +1,7 @@ import { Avatar as AntAvatar } from 'antd' import { type AvatarProps as AntAvatarProps } from 'antd' import { ConfigProvider } from 'src/components' +import './avatar.css' export interface IAvatarProps extends AntAvatarProps {} diff --git a/src/components/data-display/Avatar/avatar.css b/src/components/data-display/Avatar/avatar.css new file mode 100644 index 0000000000..b838cf564f --- /dev/null +++ b/src/components/data-display/Avatar/avatar.css @@ -0,0 +1,9 @@ +/* Remove transform for dots on circular avatars */ +.ant-badge:has(.ant-avatar-circle) .ant-badge-dot { + transform: none; +} + +.ant-badge:has(.ant-avatar-circle) .ant-badge-count:not(.ant-badge-dot) { + transform: translate(50%, -50%); +} + From f49b1ffacf196fd4925b000b244c8508a861e9fe Mon Sep 17 00:00:00 2001 From: Anastasiia Stavitskaya Date: Mon, 12 Jan 2026 14:03:27 -0800 Subject: [PATCH 15/15] feat: radio group component (#712) Co-authored-by: Jrad --- .../RadioCard/RadioCard.stories.tsx | 120 +++++++++++++++ .../data-entry/RadioCard/RadioCard.tsx | 138 ++++++++++++++++++ src/components/index.ts | 1 + 3 files changed, 259 insertions(+) create mode 100644 src/components/data-entry/RadioCard/RadioCard.stories.tsx create mode 100644 src/components/data-entry/RadioCard/RadioCard.tsx diff --git a/src/components/data-entry/RadioCard/RadioCard.stories.tsx b/src/components/data-entry/RadioCard/RadioCard.stories.tsx new file mode 100644 index 0000000000..1a20a3771d --- /dev/null +++ b/src/components/data-entry/RadioCard/RadioCard.stories.tsx @@ -0,0 +1,120 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' +import { RadioCards } from './RadioCard' +import { Center } from 'src/components' + +const meta: Meta = { + title: 'Components/Data Entry/RadioCard', + component: RadioCards, + parameters: { + layout: 'padded', + }, +} + +export default meta +type Story = StoryObj + +export const Vertical: Story = { + render: () => { + const [value, setValue] = useState('secret') + + return ( +
    + +
    + ) + }, +} + +export const Horizontal: Story = { + render: () => { + const [value, setValue] = useState('warehouse') + + return ( +
    + +
    + ) + }, +} + +export const Disabled: Story = { + render: () => { + const [value, setValue] = useState('standard') + + return ( +
    + +
    + ) + }, +} diff --git a/src/components/data-entry/RadioCard/RadioCard.tsx b/src/components/data-entry/RadioCard/RadioCard.tsx new file mode 100644 index 0000000000..d0d35e0b7a --- /dev/null +++ b/src/components/data-entry/RadioCard/RadioCard.tsx @@ -0,0 +1,138 @@ +import React from 'react' +import { Card } from 'src/components/data-display/Card/Card' +import { Tooltip } from 'src/components/data-display/Tooltip/Tooltip' +import { Typography } from 'src/components/general/Typography/Typography' +import { Flex } from 'src/components/layout/Flex/Flex' +import { Radio } from 'src/components/data-entry/Radio/Radio' +import { + ColorBorderSecondary, + ColorPrimary, + LineType, + LineWidth, + SizeXs, + Size, + OpacityLoading, + Padding, +} from 'src/styles/style' + +export interface RadioCardOption { + value: T + title: React.ReactNode + description?: React.ReactNode + disabled?: boolean + tooltipTitle?: React.ReactNode + testId?: string +} + +export interface RadioCardsProps { + options: Array> + value?: T + onChange?: (value: T) => void + disabled?: boolean + orientation?: 'vertical' | 'horizontal' + radioAlign?: 'start' | 'center' + gap?: number | string +} + +interface RadioCardProps { + value: string + title: React.ReactNode + description?: React.ReactNode + disabled?: boolean + checked?: boolean + onChange?: (value: string) => void + radioAlign?: 'start' | 'center' +} + +const RadioCard: React.FC = ({ + title, + description, + value, + checked = false, + onChange, + radioAlign = 'start', + disabled = false, +}) => { + const handleClick = () => { + if (!disabled && onChange) { + onChange(value) + } + } + + return ( + + + + + {title} + {description} + + + + ) +} + +export const RadioCards = ({ + options, + value, + onChange, + disabled = false, + orientation = 'vertical', + radioAlign = 'start', + gap = Size, +}: RadioCardsProps) => { + return ( + + {options.map(option => { + const isDisabled = disabled || option.disabled + const isSelected = value === option.value + const cardValue = String(option.value) + + const card = ( + onChange?.(option.value)} + radioAlign={radioAlign} + disabled={isDisabled} + /> + ) + + return ( + + {option.tooltipTitle ? {card} : card} + + ) + })} + + ) +} diff --git a/src/components/index.ts b/src/components/index.ts index a4bd6790e2..f2508f67ce 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -15,6 +15,7 @@ export { } from './data-entry/Select/Select' export { Mentions, type IMentionsProps } from './not-prod-ready/Mentions/Mentions' export { Radio, type IRadioProps } from './data-entry/Radio/Radio' +export { RadioCards, type RadioCardsProps, type RadioCardOption } from './data-entry/RadioCard/RadioCard' export { ColorPicker, type IColorPickerProps } from './not-prod-ready/ColorPicker/ColorPicker' export { Slider, type ISliderProps } from './not-prod-ready/Slider/Slider' export { Cascader, type ICascaderProps } from './data-entry/Cascader/Cascader'