Merge pull request #30335 from backstage/canon-use-style

Canon - Bring styles into a single place
This commit is contained in:
Ben Lambert
2025-06-24 09:10:47 +02:00
committed by GitHub
34 changed files with 1284 additions and 428 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': patch
---
We are transforming how we structure our class names and data attributes definitions for all components. They are now all set in the same place.
+250
View File
@@ -245,6 +245,9 @@ export interface CheckboxProps {
value?: string;
}
// @public
export type ClassNamesMap = Record<string, string>;
// @public
export const Collapsible: {
Root: ForwardRefExoticComponent<
@@ -267,6 +270,247 @@ export const Collapsible: {
// @public (undocumented)
export type Columns = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 'auto';
// @public
export type ComponentClassNames<T extends ComponentDefinitionName> =
(typeof componentDefinitions)[T]['classNames'];
// @public
export interface ComponentDefinition {
// (undocumented)
classNames: ClassNamesMap;
// (undocumented)
dataAttributes?: DataAttributesMap;
}
// @public
export type ComponentDefinitionName = keyof typeof componentDefinitions;
// @public
export const componentDefinitions: {
readonly Avatar: {
readonly classNames: {
readonly root: 'canon-AvatarRoot';
readonly image: 'canon-AvatarImage';
readonly fallback: 'canon-AvatarFallback';
};
readonly dataAttributes: {
readonly size: readonly ['small', 'medium', 'large'];
};
};
readonly Box: {
readonly classNames: {
readonly root: 'canon-Box';
};
};
readonly Button: {
readonly classNames: {
readonly root: 'canon-Button';
};
readonly dataAttributes: {
readonly size: readonly ['small', 'medium', 'large'];
readonly variant: readonly ['primary', 'secondary', 'ghost'];
};
};
readonly ButtonIcon: {
readonly classNames: {
readonly root: 'canon-ButtonIcon';
};
};
readonly ButtonLink: {
readonly classNames: {
readonly root: 'canon-ButtonLink';
};
};
readonly Checkbox: {
readonly classNames: {
readonly root: 'canon-CheckboxRoot';
readonly label: 'canon-CheckboxLabel';
readonly indicator: 'canon-CheckboxIndicator';
};
readonly dataAttributes: {
readonly checked: readonly [true, false];
};
};
readonly Collapsible: {
readonly classNames: {
readonly root: 'canon-CollapsibleRoot';
readonly trigger: 'canon-CollapsibleTrigger';
readonly panel: 'canon-CollapsiblePanel';
};
};
readonly Container: {
readonly classNames: {
readonly root: 'canon-Container';
};
};
readonly FieldLabel: {
readonly classNames: {
readonly root: 'canon-FieldLabel';
readonly label: 'canon-FieldLabelLabel';
readonly secondaryLabel: 'canon-FieldLabelSecondaryLabel';
readonly description: 'canon-FieldLabelDescription';
};
};
readonly Flex: {
readonly classNames: {
readonly root: 'canon-Flex';
};
};
readonly Grid: {
readonly classNames: {
readonly root: 'canon-Grid';
readonly item: 'canon-GridItem';
};
};
readonly Heading: {
readonly classNames: {
readonly root: 'canon-Heading';
};
readonly dataAttributes: {
readonly variant: readonly ['title1', 'title2', 'title3', 'subtitle'];
readonly color: readonly ['primary', 'secondary', 'muted'];
readonly truncate: readonly [true, false];
};
};
readonly Icon: {
readonly classNames: {
readonly root: 'canon-Icon';
};
};
readonly Link: {
readonly classNames: {
readonly root: 'canon-Link';
};
readonly dataAttributes: {
readonly variant: readonly ['subtitle', 'body', 'caption', 'label'];
readonly weight: readonly ['regular', 'bold'];
};
};
readonly Menu: {
readonly classNames: {
readonly trigger: 'canon-MenuTrigger';
readonly backdrop: 'canon-MenuBackdrop';
readonly positioner: 'canon-MenuPositioner';
readonly popup: 'canon-MenuPopup';
readonly arrow: 'canon-MenuArrow';
readonly item: 'canon-MenuItem';
readonly group: 'canon-MenuGroup';
readonly groupLabel: 'canon-MenuGroupLabel';
readonly radioGroup: 'canon-MenuRadioGroup';
readonly radioItem: 'canon-MenuRadioItem';
readonly radioItemIndicator: 'canon-MenuRadioItemIndicator';
readonly checkboxItem: 'canon-MenuCheckboxItem';
readonly checkboxItemIndicator: 'canon-MenuCheckboxItemIndicator';
readonly submenuTrigger: 'canon-MenuSubmenuTrigger';
readonly separator: 'canon-MenuSeparator';
};
};
readonly RadioGroup: {
readonly classNames: {
readonly root: 'canon-RadioGroup';
readonly content: 'canon-RadioGroupContent';
readonly radio: 'canon-Radio';
};
};
readonly ScrollArea: {
readonly classNames: {
readonly root: 'canon-ScrollAreaRoot';
readonly viewport: 'canon-ScrollAreaViewport';
readonly scrollbar: 'canon-ScrollAreaScrollbar';
readonly thumb: 'canon-ScrollAreaThumb';
};
};
readonly Select: {
readonly classNames: {
readonly root: 'canon-Select';
readonly required: 'canon-SelectRequired';
readonly trigger: 'canon-SelectTrigger';
readonly value: 'canon-SelectValue';
readonly icon: 'canon-SelectIcon';
readonly popup: 'canon-SelectPopup';
readonly item: 'canon-SelectItem';
readonly itemIndicator: 'canon-SelectItemIndicator';
readonly itemText: 'canon-SelectItemText';
readonly description: 'canon-SelectDescription';
readonly error: 'canon-SelectError';
};
readonly dataAttributes: {
readonly size: readonly ['small', 'medium'];
};
};
readonly Switch: {
readonly classNames: {
readonly root: 'canon-Switch';
readonly indicator: 'canon-SwitchIndicator';
};
};
readonly Table: {
readonly classNames: {
readonly root: 'canon-TableRoot';
readonly header: 'canon-TableHeader';
readonly body: 'canon-TableBody';
readonly row: 'canon-TableRow';
readonly head: 'canon-TableHead';
readonly caption: 'canon-TableCaption';
readonly cell: 'canon-TableCell';
readonly cellText: 'canon-TableCellText';
readonly cellLink: 'canon-TableCellLink';
readonly cellProfile: 'canon-TableCellProfile';
readonly cellProfileAvatar: 'canon-TableCellProfileAvatar';
readonly cellProfileAvatarImage: 'canon-TableCellProfileAvatarImage';
readonly cellProfileAvatarFallback: 'canon-TableCellProfileAvatarFallback';
readonly cellProfileName: 'canon-TableCellProfileName';
readonly cellProfileLink: 'canon-TableCellProfileLink';
};
};
readonly Tabs: {
readonly classNames: {
readonly root: 'canon-TabsRoot';
readonly list: 'canon-TabsList';
readonly indicator: 'canon-TabsIndicator';
readonly tab: 'canon-TabsTab';
readonly panel: 'canon-TabsPanel';
};
};
readonly Text: {
readonly classNames: {
readonly root: 'canon-Text';
};
readonly dataAttributes: {
readonly variant: readonly ['subtitle', 'body', 'caption', 'label'];
readonly weight: readonly ['regular', 'bold'];
readonly color: readonly [
'primary',
'secondary',
'danger',
'warning',
'success',
];
readonly truncate: readonly [true, false];
};
};
readonly TextField: {
readonly classNames: {
readonly root: 'canon-TextField';
readonly inputWrapper: 'canon-TextFieldInputWrapper';
readonly icon: 'canon-TextFieldIcon';
readonly input: 'canon-TextFieldInput';
};
readonly dataAttributes: {
readonly invalid: readonly [true, false];
readonly disabled: readonly [true, false];
};
};
readonly Tooltip: {
readonly classNames: {
readonly trigger: 'canon-TooltipTrigger';
readonly positioner: 'canon-TooltipPositioner';
readonly popup: 'canon-TooltipPopup';
readonly arrow: 'canon-TooltipArrow';
};
};
};
// @public (undocumented)
export const Container: ForwardRefExoticComponent<
ContainerProps & RefAttributes<HTMLDivElement>
@@ -294,6 +538,12 @@ export interface ContainerProps {
style?: React.CSSProperties;
}
// @public
export type DataAttributesMap = Record<string, DataAttributeValues>;
// @public
export type DataAttributeValues = readonly (string | number | boolean)[];
// @public
export const DataTable: {
Root: <TData>(
+26 -18
View File
@@ -18,28 +18,36 @@ import { forwardRef, ElementRef } from 'react';
import { Avatar as AvatarPrimitive } from '@base-ui-components/react/avatar';
import clsx from 'clsx';
import { AvatarProps } from './types';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Avatar = forwardRef<
ElementRef<typeof AvatarPrimitive.Root>,
AvatarProps
>(({ className, src, name, size = 'medium', ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={clsx('canon-AvatarRoot', className)}
data-size={size}
{...props}
>
<AvatarPrimitive.Image className="canon-AvatarImage" src={src} />
<AvatarPrimitive.Fallback className="canon-AvatarFallback">
{(name || '')
.split(' ')
.map(word => word[0])
.join('')
.toLocaleUpperCase('en-US')
.slice(0, 2)}
</AvatarPrimitive.Fallback>
</AvatarPrimitive.Root>
));
>((props, ref) => {
const { className, src, name, size = 'medium', ...rest } = props;
const { classNames } = useStyles('Avatar', {
size,
});
return (
<AvatarPrimitive.Root
ref={ref}
className={clsx(classNames.root, className)}
data-size={size}
{...rest}
>
<AvatarPrimitive.Image className={classNames.image} src={src} />
<AvatarPrimitive.Fallback className={classNames.fallback}>
{(name || '')
.split(' ')
.map(word => word[0])
.join('')
.toLocaleUpperCase('en-US')
.slice(0, 2)}
</AvatarPrimitive.Fallback>
</AvatarPrimitive.Root>
);
});
Avatar.displayName = AvatarPrimitive.Root.displayName;
+3 -1
View File
@@ -24,6 +24,7 @@ import { widthPropDefs } from '../../props/width.props';
import { heightPropDefs } from '../../props/height.props';
import { positionPropDefs } from '../../props/position.props';
import { displayPropDefs } from '../../props/display.props';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
@@ -38,11 +39,12 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
...boxPropDefs,
};
const { classNames } = useStyles('Box');
const { className, style } = extractProps(props, propDefs);
return createElement(props.as || 'div', {
ref,
className: clsx('canon-Box', className),
className: clsx(classNames.root, className),
style,
children,
});
@@ -17,8 +17,8 @@
import clsx from 'clsx';
import { forwardRef, Ref } from 'react';
import { Button as RAButton } from 'react-aria-components';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { ButtonProps } from './types';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Button = forwardRef(
@@ -33,15 +33,16 @@ export const Button = forwardRef(
...rest
} = props;
const responsiveSize = useResponsiveValue(size);
const responsiveVariant = useResponsiveValue(variant);
const { classNames, dataAttributes } = useStyles('Button', {
size,
variant,
});
return (
<RAButton
className={clsx('canon-Button', className)}
data-variant={responsiveVariant}
data-size={responsiveSize}
className={clsx(classNames.root, className)}
ref={ref}
{...dataAttributes}
{...rest}
>
{iconStart}
@@ -17,8 +17,8 @@
import clsx from 'clsx';
import { forwardRef, Ref } from 'react';
import { Button as RAButton } from 'react-aria-components';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { ButtonIconProps } from './types';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const ButtonIcon = forwardRef(
@@ -32,15 +32,18 @@ export const ButtonIcon = forwardRef(
...rest
} = props;
const responsiveSize = useResponsiveValue(size);
const responsiveVariant = useResponsiveValue(variant);
const { classNames, dataAttributes } = useStyles('Button', {
size,
variant,
});
const { classNames: classNamesButtonIcon } = useStyles('ButtonIcon');
return (
<RAButton
className={clsx('canon-Button', 'canon-ButtonIcon', className)}
data-variant={responsiveVariant}
data-size={responsiveSize}
className={clsx(classNames.root, classNamesButtonIcon.root, className)}
ref={ref}
{...dataAttributes}
{...rest}
>
{icon}
@@ -17,8 +17,8 @@
import clsx from 'clsx';
import { forwardRef, Ref } from 'react';
import { Link as RALink } from 'react-aria-components';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { ButtonLinkProps } from './types';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const ButtonLink = forwardRef(
@@ -33,15 +33,18 @@ export const ButtonLink = forwardRef(
...rest
} = props;
const responsiveSize = useResponsiveValue(size);
const responsiveVariant = useResponsiveValue(variant);
const { classNames, dataAttributes } = useStyles('Button', {
size,
variant,
});
const { classNames: classNamesButtonLink } = useStyles('ButtonLink');
return (
<RALink
className={clsx('canon-Button', 'canon-ButtonLink', className)}
data-variant={responsiveVariant}
data-size={responsiveSize}
className={clsx(classNames.root, classNamesButtonLink.root, className)}
ref={ref}
{...dataAttributes}
{...rest}
>
{iconStart}
@@ -18,6 +18,7 @@ import { forwardRef } from 'react';
import { Checkbox as CheckboxPrimitive } from '@base-ui-components/react/checkbox';
import { Icon } from '@backstage/canon';
import type { CheckboxProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import clsx from 'clsx';
/** @public */
@@ -35,10 +36,12 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
style,
} = props;
const { classNames } = useStyles('Checkbox');
const checkboxElement = (
<CheckboxPrimitive.Root
ref={ref}
className={clsx('canon-CheckboxRoot', className)}
className={clsx(classNames.root, className)}
checked={checked}
onCheckedChange={onChange}
disabled={disabled}
@@ -47,14 +50,14 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
value={value}
style={style}
>
<CheckboxPrimitive.Indicator className="canon-CheckboxIndicator">
<CheckboxPrimitive.Indicator className={classNames.indicator}>
<Icon name="check" size={12} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
return label ? (
<label className="canon-CheckboxLabel">
<label className={classNames.label}>
{checkboxElement}
{label}
</label>
@@ -17,41 +17,54 @@
import { forwardRef } from 'react';
import { Collapsible as CollapsiblePrimitive } from '@base-ui-components/react/collapsible';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
const CollapsibleRoot = forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Root>
>(({ className, ...props }, ref) => (
<CollapsiblePrimitive.Root
ref={ref}
className={clsx('canon-CollapsibleRoot', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Collapsible');
return (
<CollapsiblePrimitive.Root
ref={ref}
className={clsx(classNames.root, className)}
{...props}
/>
);
});
CollapsibleRoot.displayName = CollapsiblePrimitive.Root.displayName;
const CollapsibleTrigger = forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger>
>(({ className, ...props }, ref) => (
<CollapsiblePrimitive.Trigger
ref={ref}
className={clsx('canon-CollapsibleTrigger', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Collapsible');
return (
<CollapsiblePrimitive.Trigger
ref={ref}
className={clsx(classNames.trigger, className)}
{...props}
/>
);
});
CollapsibleTrigger.displayName = CollapsiblePrimitive.Trigger.displayName;
const CollapsiblePanel = forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.Panel>,
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Panel>
>(({ className, ...props }, ref) => (
<CollapsiblePrimitive.Panel
ref={ref}
className={clsx('canon-CollapsiblePanel', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Collapsible');
return (
<CollapsiblePrimitive.Panel
ref={ref}
className={clsx(classNames.panel, className)}
{...props}
/>
);
});
CollapsiblePanel.displayName = CollapsiblePrimitive.Panel.displayName;
/**
@@ -20,12 +20,15 @@ import clsx from 'clsx';
import { displayPropDefs } from '../../props/display.props';
import { extractProps } from '../../utils/extractProps';
import { spacingPropDefs } from '../../props/spacing.props';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Container = forwardRef<HTMLDivElement, ContainerProps>(
(props, ref) => {
const { children } = props;
const { classNames } = useStyles('Container');
// Create a subset of spacing props that match the interface
const containerSpacingProps = {
my: spacingPropDefs.my,
@@ -44,7 +47,7 @@ export const Container = forwardRef<HTMLDivElement, ContainerProps>(
return createElement('div', {
ref,
className: clsx('canon-Container', className),
className: clsx(classNames.root, className),
style,
children,
});
@@ -16,31 +16,31 @@
import { Label } from 'react-aria-components';
import { forwardRef } from 'react';
import type { FieldLabelProps } from './types';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const FieldLabel = forwardRef<HTMLDivElement, FieldLabelProps>(
(props: FieldLabelProps, ref) => {
const { label, secondaryLabel, description, htmlFor, id, ...rest } = props;
const { classNames } = useStyles('FieldLabel');
if (!label) return null;
return (
<div className="canon-FieldLabel" {...rest} ref={ref}>
<div className={classNames.root} {...rest} ref={ref}>
{label && (
<Label className="canon-FieldLabelLabel" htmlFor={htmlFor} id={id}>
<Label className={classNames.label} htmlFor={htmlFor} id={id}>
{label}
{secondaryLabel && (
<span
aria-hidden="true"
className="canon-FieldLabelSecondaryLabel"
>
<span aria-hidden="true" className={classNames.secondaryLabel}>
({secondaryLabel})
</span>
)}
</Label>
)}
{description && (
<div className="canon-FieldLabelDescription">{description}</div>
<div className={classNames.description}>{description}</div>
)}
</div>
);
+3 -1
View File
@@ -21,6 +21,7 @@ import { flexPropDefs } from './Flex.props';
import { extractProps } from '../../utils/extractProps';
import { gapPropDefs } from '../../props/gap-props';
import { spacingPropDefs } from '../../props/spacing.props';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
@@ -30,11 +31,12 @@ export const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
...spacingPropDefs,
};
const { classNames } = useStyles('Flex');
const { className, style } = extractProps(props, propDefs);
return createElement('div', {
ref,
className: clsx('canon-Flex', className),
className: clsx(classNames.root, className),
style,
children: props.children,
});
+6 -2
View File
@@ -21,6 +21,7 @@ import { gridItemPropDefs, gridPropDefs } from './Grid.props';
import clsx from 'clsx';
import type { GridItemProps, GridProps } from './types';
import { spacingPropDefs } from '../../props/spacing.props';
import { useStyles } from '../../hooks/useStyles';
const GridRoot = forwardRef<HTMLDivElement, GridProps>((props, ref) => {
const propDefs = {
@@ -29,11 +30,13 @@ const GridRoot = forwardRef<HTMLDivElement, GridProps>((props, ref) => {
...spacingPropDefs,
};
const { classNames } = useStyles('Grid');
const { className, style } = extractProps(props, propDefs);
return createElement('div', {
ref,
className: clsx('canon-Grid', className),
className: clsx(classNames.root, className),
style,
children: props.children,
});
@@ -44,11 +47,12 @@ const GridItem = forwardRef<HTMLDivElement, GridItemProps>((props, ref) => {
...gridItemPropDefs,
};
const { classNames } = useStyles('Grid');
const { className, style } = extractProps(props, propDefs);
return createElement('div', {
ref,
className: clsx('canon-GridItem', className),
className: clsx(classNames.item, className),
style,
children: props.children,
});
@@ -16,9 +16,9 @@
import { forwardRef } from 'react';
import clsx from 'clsx';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import type { ElementType } from 'react';
import type { HeadingProps } from './types';
import { useStyles } from '../../hooks/useStyles';
function HeadingComponent<T extends ElementType = 'h1'>(
{
@@ -34,16 +34,17 @@ function HeadingComponent<T extends ElementType = 'h1'>(
) {
const Component = as || 'h1';
const responsiveVariant = useResponsiveValue(variant);
const responsiveColor = useResponsiveValue(color);
const { classNames, dataAttributes } = useStyles('Heading', {
variant,
color,
truncate,
});
return (
<Component
ref={ref}
className={clsx('canon-Heading', className)}
data-variant={responsiveVariant}
data-color={responsiveColor}
data-truncate={truncate}
className={clsx(classNames.root, className)}
{...dataAttributes}
style={style}
{...restProps}
/>
+4 -1
View File
@@ -18,6 +18,7 @@ import { ComponentType } from 'react';
import { useIcons } from './context';
import type { IconProps } from './types';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Icon = (props: IconProps) => {
@@ -31,9 +32,11 @@ export const Icon = (props: IconProps) => {
return null;
}
const { classNames } = useStyles('Icon');
return (
<CanonIcon
className={clsx('canon-Icon', className)}
className={clsx(classNames.root, className)}
style={{
...(size ? { width: size, height: size } : {}),
...style,
+4 -6
View File
@@ -16,8 +16,8 @@
import { useRef, forwardRef } from 'react';
import { useRender } from '@base-ui-components/react/use-render';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import type { LinkProps } from './types';
@@ -31,16 +31,14 @@ export const Link = forwardRef<HTMLElement, LinkProps>((props, ref) => {
...restProps
} = props;
const responsiveVariant = useResponsiveValue(variant);
const responsiveWeight = useResponsiveValue(weight);
const { classNames, dataAttributes } = useStyles('Link', { variant, weight });
const internalRef = useRef<HTMLElement | null>(null);
const { renderElement } = useRender({
render,
props: {
className: clsx('canon-Link', className),
['data-variant']: responsiveVariant,
['data-weight']: responsiveWeight,
className: clsx(classNames.root, className),
...dataAttributes,
...restProps,
},
refs: [ref, internalRef],
+173 -108
View File
@@ -20,190 +20,255 @@ import clsx from 'clsx';
import { MenuComponent } from './types';
import { Combobox } from './Combobox';
import { Icon } from '../Icon';
import { useStyles } from '../../hooks/useStyles';
const MenuTrigger = forwardRef<
React.ElementRef<typeof MenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Trigger
ref={ref}
className={clsx('canon-MenuTrigger', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.Trigger
ref={ref}
className={clsx(classNames.trigger, className)}
{...props}
/>
);
});
MenuTrigger.displayName = MenuPrimitive.Trigger.displayName;
const MenuBackdrop = forwardRef<
React.ElementRef<typeof MenuPrimitive.Backdrop>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Backdrop>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Backdrop
ref={ref}
className={clsx('canon-MenuBackdrop', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.Backdrop
ref={ref}
className={clsx(classNames.backdrop, className)}
{...props}
/>
);
});
MenuBackdrop.displayName = MenuPrimitive.Backdrop.displayName;
const MenuPositioner = forwardRef<
React.ElementRef<typeof MenuPrimitive.Positioner>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Positioner>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Positioner
ref={ref}
className={clsx('canon-MenuPositioner', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.Positioner
ref={ref}
className={clsx(classNames.positioner, className)}
{...props}
/>
);
});
MenuPositioner.displayName = MenuPrimitive.Positioner.displayName;
const MenuPopup = forwardRef<
React.ElementRef<typeof MenuPrimitive.Popup>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Popup>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Popup
ref={ref}
className={clsx('canon-MenuPopup', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.Popup
ref={ref}
className={clsx(classNames.popup, className)}
{...props}
/>
);
});
MenuPopup.displayName = MenuPrimitive.Popup.displayName;
const MenuArrow = forwardRef<
React.ElementRef<typeof MenuPrimitive.Arrow>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Arrow>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Arrow
ref={ref}
className={clsx('canon-MenuArrow', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.Arrow
ref={ref}
className={clsx(classNames.arrow, className)}
{...props}
/>
);
});
MenuArrow.displayName = MenuPrimitive.Arrow.displayName;
const MenuItem = forwardRef<
React.ElementRef<typeof MenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Item>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Item
ref={ref}
className={clsx('canon-MenuItem', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.Item
ref={ref}
className={clsx(classNames.item, className)}
{...props}
/>
);
});
MenuItem.displayName = MenuPrimitive.Item.displayName;
const MenuGroup = forwardRef<
React.ElementRef<typeof MenuPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Group>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Group
ref={ref}
className={clsx('canon-MenuGroup', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.Group
ref={ref}
className={clsx(classNames.group, className)}
{...props}
/>
);
});
MenuGroup.displayName = MenuPrimitive.Group.displayName;
const MenuGroupLabel = forwardRef<
React.ElementRef<typeof MenuPrimitive.GroupLabel>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.GroupLabel>
>(({ className, ...props }, ref) => (
<MenuPrimitive.GroupLabel
ref={ref}
className={clsx('canon-MenuGroupLabel', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.GroupLabel
ref={ref}
className={clsx(classNames.groupLabel, className)}
{...props}
/>
);
});
MenuGroupLabel.displayName = MenuPrimitive.GroupLabel.displayName;
const MenuRadioGroup = forwardRef<
React.ElementRef<typeof MenuPrimitive.RadioGroup>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.RadioGroup>
>(({ className, ...props }, ref) => (
<MenuPrimitive.RadioGroup
ref={ref}
className={clsx('canon-MenuRadioGroup', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.RadioGroup
ref={ref}
className={clsx(classNames.radioGroup, className)}
{...props}
/>
);
});
MenuRadioGroup.displayName = MenuPrimitive.RadioGroup.displayName;
const MenuRadioItem = forwardRef<
React.ElementRef<typeof MenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.RadioItem>
>(({ className, ...props }, ref) => (
<MenuPrimitive.RadioItem
ref={ref}
className={clsx('canon-MenuRadioItem', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.RadioItem
ref={ref}
className={clsx(classNames.radioItem, className)}
{...props}
/>
);
});
MenuRadioItem.displayName = MenuPrimitive.RadioItem.displayName;
const MenuRadioItemIndicator = forwardRef<
React.ElementRef<typeof MenuPrimitive.RadioItemIndicator>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.RadioItemIndicator>
>(({ className, ...props }, ref) => (
<MenuPrimitive.RadioItemIndicator
ref={ref}
className={clsx('canon-MenuRadioItemIndicator', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.RadioItemIndicator
ref={ref}
className={clsx(classNames.radioItemIndicator, className)}
{...props}
/>
);
});
MenuRadioItemIndicator.displayName =
MenuPrimitive.RadioItemIndicator.displayName;
const MenuCheckboxItem = forwardRef<
React.ElementRef<typeof MenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.CheckboxItem>
>(({ className, ...props }, ref) => (
<MenuPrimitive.CheckboxItem
ref={ref}
className={clsx('canon-MenuCheckboxItem', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.CheckboxItem
ref={ref}
className={clsx(classNames.checkboxItem, className)}
{...props}
/>
);
});
MenuCheckboxItem.displayName = MenuPrimitive.CheckboxItem.displayName;
const MenuCheckboxItemIndicator = forwardRef<
React.ElementRef<typeof MenuPrimitive.CheckboxItemIndicator>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.CheckboxItemIndicator>
>(({ className, ...props }, ref) => (
<MenuPrimitive.CheckboxItemIndicator
ref={ref}
className={clsx('canon-MenuCheckboxItemIndicator', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.CheckboxItemIndicator
ref={ref}
className={clsx(classNames.checkboxItemIndicator, className)}
{...props}
/>
);
});
MenuCheckboxItemIndicator.displayName =
MenuPrimitive.CheckboxItemIndicator.displayName;
const MenuSubmenuTrigger = forwardRef<
React.ElementRef<typeof MenuPrimitive.SubmenuTrigger>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.SubmenuTrigger>
>(({ className, children, ...props }, ref) => (
<MenuPrimitive.SubmenuTrigger
ref={ref}
className={clsx('canon-MenuSubmenuTrigger', className)}
{...props}
>
<div>{children}</div>
<Icon aria-label="Submenu indicator icon" name="chevron-right" size={20} />
</MenuPrimitive.SubmenuTrigger>
));
>(({ className, children, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.SubmenuTrigger
ref={ref}
className={clsx(classNames.submenuTrigger, className)}
{...props}
>
<div>{children}</div>
<Icon
aria-label="Submenu indicator icon"
name="chevron-right"
size={20}
/>
</MenuPrimitive.SubmenuTrigger>
);
});
MenuSubmenuTrigger.displayName = MenuPrimitive.SubmenuTrigger.displayName;
const MenuSeparator = forwardRef<
React.ElementRef<typeof MenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenuPrimitive.Separator
ref={ref}
className={clsx('canon-MenuSeparator', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Menu');
return (
<MenuPrimitive.Separator
ref={ref}
className={clsx(classNames.separator, className)}
{...props}
/>
);
});
MenuSeparator.displayName = MenuPrimitive.Separator.displayName;
/** @public */
@@ -22,6 +22,7 @@ import {
import clsx from 'clsx';
import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import { useStyles } from '../../hooks/useStyles';
import type { RadioGroupProps, RadioProps } from './types';
@@ -40,6 +41,8 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
...rest
} = props;
const { classNames } = useStyles('RadioGroup');
useEffect(() => {
if (!label && !ariaLabel && !ariaLabelledBy) {
console.warn(
@@ -54,7 +57,7 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
return (
<AriaRadioGroup
className={clsx('canon-RadioGroup', className)}
className={clsx(classNames.root, className)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
{...rest}
@@ -65,7 +68,7 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
secondaryLabel={secondaryLabelText}
description={description}
/>
<div className="canon-RadioGroupContent">{children}</div>
<div className={classNames.content}>{children}</div>
<FieldError />
</AriaRadioGroup>
);
@@ -78,8 +81,14 @@ RadioGroup.displayName = 'RadioGroup';
export const Radio = forwardRef<HTMLLabelElement, RadioProps>((props, ref) => {
const { className, ...rest } = props;
const { classNames } = useStyles('RadioGroup');
return (
<AriaRadio className={clsx('canon-Radio', className)} {...rest} ref={ref} />
<AriaRadio
className={clsx(classNames.radio, className)}
{...rest}
ref={ref}
/>
);
});
@@ -17,53 +17,70 @@
import { forwardRef } from 'react';
import { ScrollArea as ScrollAreaPrimitive } from '@base-ui-components/react/scroll-area';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
const ScrollAreaRoot = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={clsx('canon-ScrollAreaRoot', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('ScrollArea');
return (
<ScrollAreaPrimitive.Root
ref={ref}
className={clsx(classNames.root, className)}
{...props}
/>
);
});
ScrollAreaRoot.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollAreaViewport = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<ScrollAreaPrimitive.Viewport
ref={ref}
className={clsx('canon-ScrollAreaViewport', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('ScrollArea');
return (
<ScrollAreaPrimitive.Viewport
ref={ref}
className={clsx(classNames.viewport, className)}
{...props}
/>
);
});
ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName;
const ScrollAreaScrollbar = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Scrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar>
>(({ className, ...props }, ref) => (
<ScrollAreaPrimitive.Scrollbar
ref={ref}
className={clsx('canon-ScrollAreaScrollbar', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('ScrollArea');
return (
<ScrollAreaPrimitive.Scrollbar
ref={ref}
className={clsx(classNames.scrollbar, className)}
{...props}
/>
);
});
ScrollAreaScrollbar.displayName = ScrollAreaPrimitive.Scrollbar.displayName;
const ScrollAreaThumb = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Thumb>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Thumb>
>(({ className, ...props }, ref) => (
<ScrollAreaPrimitive.Thumb
ref={ref}
className={clsx('canon-ScrollAreaThumb', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('ScrollArea');
return (
<ScrollAreaPrimitive.Thumb
ref={ref}
className={clsx(classNames.thumb, className)}
{...props}
/>
);
});
ScrollAreaThumb.displayName = ScrollAreaPrimitive.Thumb.displayName;
/** @public */
+18 -15
View File
@@ -20,7 +20,7 @@ import { Icon } from '../Icon';
import clsx from 'clsx';
import './Select.styles.css';
import { SelectProps } from './types';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
@@ -37,8 +37,9 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
...rest
} = props;
// Get the responsive value for the variant
const responsiveSize = useResponsiveValue(size);
const { classNames, dataAttributes } = useStyles('Select', {
size,
});
// Generate unique IDs for accessibility
const selectId = useId();
@@ -58,7 +59,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
);
return (
<div className={clsx('canon-Select', className)} style={style} ref={ref}>
<div className={clsx(classNames.root, className)} style={style} ref={ref}>
{label && (
<label
className="canon-SelectLabel"
@@ -68,7 +69,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
>
{label}
{required && (
<span aria-hidden="true" className="canon-SelectRequired">
<span aria-hidden="true" className={classNames.required}>
(Required)
</span>
)}
@@ -78,33 +79,35 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
<SelectPrimitive.Trigger
ref={triggerRef}
id={selectId}
className="canon-SelectTrigger"
data-size={responsiveSize}
className={classNames.trigger}
data-size={dataAttributes['data-size']}
data-invalid={error}
>
<SelectPrimitive.Value
className="canon-SelectValue"
className={classNames.value}
placeholder={placeholder}
/>
<SelectPrimitive.Icon className="canon-SelectIcon">
<SelectPrimitive.Icon className={classNames.icon}>
<Icon name="chevron-down" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal>
<SelectPrimitive.Backdrop />
<SelectPrimitive.Positioner>
<SelectPrimitive.Popup className="canon-SelectPopup">
<SelectPrimitive.Popup className={classNames.popup}>
{options?.map(option => (
<SelectPrimitive.Item
key={option.value}
value={option.value}
disabled={option.disabled}
className="canon-SelectItem"
className={classNames.item}
>
<SelectPrimitive.ItemIndicator className="canon-SelectItemIndicator">
<SelectPrimitive.ItemIndicator
className={classNames.itemIndicator}
>
<Icon name="check" />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="canon-SelectItemText">
<SelectPrimitive.ItemText className={classNames.itemText}>
{option.label}
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
@@ -114,12 +117,12 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
{description && (
<p className="canon-SelectDescription" id={descriptionId}>
<p className={classNames.description} id={descriptionId}>
{description}
</p>
)}
{error && (
<p className="canon-SelectError" id={errorId} role="alert">
<p className={classNames.error} id={errorId} role="alert">
{error}
</p>
)}
@@ -17,13 +17,16 @@
import { forwardRef } from 'react';
import { Switch as AriaSwitch } from 'react-aria-components';
import type { SwitchProps } from './types';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const Switch = forwardRef<HTMLLabelElement, SwitchProps>(
({ label, ...props }, ref) => {
const { classNames } = useStyles('Switch');
return (
<AriaSwitch className="canon-Switch" ref={ref} {...props}>
<div className="canon-SwitchIndicator" />
<AriaSwitch className={classNames.root} ref={ref} {...props}>
<div className={classNames.indicator} />
{label}
</AriaSwitch>
);
+53 -28
View File
@@ -20,63 +20,88 @@ import { TableCell } from './TableCell/TableCell';
import { TableCellText } from './TableCellText/TableCellText';
import { TableCellLink } from './TableCellLink/TableCellLink';
import { TableCellProfile } from './TableCellProfile/TableCellProfile';
import { useStyles } from '../../hooks/useStyles';
const TableRoot = forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<table ref={ref} className={clsx('canon-TableRoot', className)} {...props} />
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<table ref={ref} className={clsx(classNames.root, className)} {...props} />
);
});
TableRoot.displayName = 'TableRoot';
const TableHeader = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead
ref={ref}
className={clsx('canon-TableHeader', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<thead
ref={ref}
className={clsx(classNames.header, className)}
{...props}
/>
);
});
TableHeader.displayName = 'TableHeader';
const TableBody = forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={clsx('canon-TableBody', className)} {...props} />
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<tbody ref={ref} className={clsx(classNames.body, className)} {...props} />
);
});
TableBody.displayName = 'TableBody';
const TableRow = forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr ref={ref} className={clsx('canon-TableRow', className)} {...props}>
{props.children}
</tr>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<tr ref={ref} className={clsx(classNames.row, className)} {...props}>
{props.children}
</tr>
);
});
TableRow.displayName = 'TableRow';
const TableHead = forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th ref={ref} className={clsx('canon-TableHead', className)} {...props} />
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<th ref={ref} className={clsx(classNames.head, className)} {...props} />
);
});
TableHead.displayName = 'TableHead';
const TableCaption = forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={clsx('canon-TableCaption', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<caption
ref={ref}
className={clsx(classNames.caption, className)}
{...props}
/>
);
});
TableCaption.displayName = 'TableCaption';
/**
@@ -16,14 +16,19 @@
import { forwardRef } from 'react';
import clsx from 'clsx';
import { useStyles } from '../../../hooks/useStyles';
/** @public */
const TableCell = forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td ref={ref} className={clsx('canon-TableCell', className)} {...props} />
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<td ref={ref} className={clsx(classNames.cell, className)} {...props} />
);
});
TableCell.displayName = 'TableCell';
export { TableCell };
@@ -19,27 +19,32 @@ import clsx from 'clsx';
import { TableCellLinkProps } from './types';
import { Text } from '../../Text/Text';
import { Link } from '../../Link/Link';
import { useStyles } from '../../../hooks/useStyles';
/** @public */
const TableCellLink = forwardRef<HTMLDivElement, TableCellLinkProps>(
({ className, title, description, href, render, ...props }, ref) => (
<div
ref={ref}
className={clsx('canon-TableCellLink', className)}
{...props}
>
{title && (
<Link href={href} render={render}>
{title}
</Link>
)}
{description && (
<Text variant="body" color="secondary">
{description}
</Text>
)}
</div>
),
({ className, title, description, href, render, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<div
ref={ref}
className={clsx(classNames.cellLink, className)}
{...props}
>
{title && (
<Link href={href} render={render}>
{title}
</Link>
)}
{description && (
<Text variant="body" color="secondary">
{description}
</Text>
)}
</div>
);
},
);
TableCellLink.displayName = 'TableCellLink';
@@ -20,40 +20,45 @@ import { TableCellProfileProps } from './types';
import { Text } from '../../Text/Text';
import { Link } from '../../Link/Link';
import { Avatar } from '@base-ui-components/react/avatar';
import { useStyles } from '../../../hooks/useStyles';
/** @public */
const TableCellProfile = forwardRef<HTMLDivElement, TableCellProfileProps>(
({ className, src, name, to, withImage = true, ...rest }, ref) => (
<div
ref={ref}
className={clsx('canon-TableCellProfile', className)}
{...rest}
>
{withImage && (
<Avatar.Root className="canon-TableCellProfileAvatar">
<Avatar.Image
src={src}
width="20"
height="20"
className="canon-TableCellProfileAvatarImage"
/>
<Avatar.Fallback className="canon-TableCellProfileAvatarFallback">
{(name || '')
.split(' ')
.map(word => word[0])
.join('')
.toLocaleUpperCase('en-US')
.slice(0, 1)}
</Avatar.Fallback>
</Avatar.Root>
)}
{name && to ? (
<Link to={to}>{name}</Link>
) : (
<Text variant="body">{name}</Text>
)}
</div>
),
({ className, src, name, to, withImage = true, ...rest }, ref) => {
const { classNames } = useStyles('Table');
return (
<div
ref={ref}
className={clsx(classNames.cellProfile, className)}
{...rest}
>
{withImage && (
<Avatar.Root className={classNames.cellProfileAvatar}>
<Avatar.Image
src={src}
width="20"
height="20"
className={classNames.cellProfileAvatarImage}
/>
<Avatar.Fallback className={classNames.cellProfileAvatarFallback}>
{(name || '')
.split(' ')
.map(word => word[0])
.join('')
.toLocaleUpperCase('en-US')
.slice(0, 1)}
</Avatar.Fallback>
</Avatar.Root>
)}
{name && to ? (
<Link to={to}>{name}</Link>
) : (
<Text variant="body">{name}</Text>
)}
</div>
);
},
);
TableCellProfile.displayName = 'TableCellProfile';
@@ -18,23 +18,28 @@ import { forwardRef } from 'react';
import clsx from 'clsx';
import { TableCellTextProps } from './types';
import { Text } from '../../Text/Text';
import { useStyles } from '../../../hooks/useStyles';
/** @public */
const TableCellText = forwardRef<HTMLDivElement, TableCellTextProps>(
({ className, title, description, ...props }, ref) => (
<div
ref={ref}
className={clsx('canon-TableCellText', className)}
{...props}
>
{title && <Text variant="body">{title}</Text>}
{description && (
<Text variant="body" color="secondary">
{description}
</Text>
)}
</div>
),
({ className, title, description, ...props }, ref) => {
const { classNames } = useStyles('Table');
return (
<div
ref={ref}
className={clsx(classNames.cellText, className)}
{...props}
>
{title && <Text variant="body">{title}</Text>}
{description && (
<Text variant="body" color="secondary">
{description}
</Text>
)}
</div>
);
},
);
TableCellText.displayName = 'TableCellText';
+48 -31
View File
@@ -18,56 +18,73 @@ import { forwardRef } from 'react';
import { Tabs as TabsPrimitive } from '@base-ui-components/react/tabs';
import type { TabsRootWithoutOrientation } from './types';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
const TabsRoot = forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
TabsRootWithoutOrientation
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root
ref={ref}
className={clsx('canon-TabsRoot', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Tabs');
return (
<TabsPrimitive.Root
ref={ref}
className={clsx(classNames.root, className)}
{...props}
/>
);
});
TabsRoot.displayName = TabsPrimitive.Root.displayName;
const TabsList = forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, children, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={clsx('canon-TabsList', className)}
{...props}
>
{children}
<TabsPrimitive.Indicator className="canon-TabsIndicator" />
</TabsPrimitive.List>
));
>(({ className, children, ...props }, ref) => {
const { classNames } = useStyles('Tabs');
return (
<TabsPrimitive.List
ref={ref}
className={clsx(classNames.list, className)}
{...props}
>
{children}
<TabsPrimitive.Indicator className={classNames.indicator} />
</TabsPrimitive.List>
);
});
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTab = forwardRef<
React.ElementRef<typeof TabsPrimitive.Tab>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Tab>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Tab
ref={ref}
className={clsx('canon-TabsTab', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Tabs');
return (
<TabsPrimitive.Tab
ref={ref}
className={clsx(classNames.tab, className)}
{...props}
/>
);
});
TabsTab.displayName = TabsPrimitive.Tab.displayName;
const TabsPanel = forwardRef<
React.ElementRef<typeof TabsPrimitive.Panel>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Panel>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Panel
ref={ref}
className={clsx('canon-TabsPanel', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Tabs');
return (
<TabsPrimitive.Panel
ref={ref}
className={clsx(classNames.panel, className)}
{...props}
/>
);
});
TabsPanel.displayName = TabsPrimitive.Panel.displayName;
/** @public */
+9 -10
View File
@@ -15,10 +15,10 @@
*/
import { forwardRef } from 'react';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import clsx from 'clsx';
import type { ElementType } from 'react';
import type { TextProps } from './types';
import { useStyles } from '../../hooks/useStyles';
function TextComponent<T extends ElementType = 'p'>(
{
@@ -35,19 +35,18 @@ function TextComponent<T extends ElementType = 'p'>(
) {
const Component = as || 'p';
// Get the responsive values for the variant and weight
const responsiveVariant = useResponsiveValue(variant);
const responsiveWeight = useResponsiveValue(weight);
const responsiveColor = useResponsiveValue(color);
const { classNames, dataAttributes } = useStyles('Text', {
variant,
weight,
color,
truncate,
});
return (
<Component
ref={ref}
className={clsx('canon-Text', className)}
data-variant={responsiveVariant}
data-weight={responsiveWeight}
data-color={responsiveColor}
data-truncate={truncate}
className={clsx(classNames.root, className)}
{...dataAttributes}
style={style}
{...restProps}
/>
@@ -16,12 +16,12 @@
import { forwardRef, useEffect } from 'react';
import { Input, TextField as AriaTextField } from 'react-aria-components';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import clsx from 'clsx';
import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import type { TextFieldProps } from './types';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
@@ -49,7 +49,9 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
}, [label, ariaLabel, ariaLabelledBy]);
// Get the responsive value for the variant
const responsiveSize = useResponsiveValue(size);
const { classNames, dataAttributes } = useStyles('TextField', {
size,
});
// If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required.
const secondaryLabelText =
@@ -57,8 +59,8 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
return (
<AriaTextField
className={clsx('canon-TextField', className)}
data-size={responsiveSize}
className={clsx(classNames.root, className)}
{...dataAttributes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
{...rest}
@@ -69,18 +71,21 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
secondaryLabel={secondaryLabelText}
description={description}
/>
<div className="canon-TextFieldInputWrapper" data-size={responsiveSize}>
<div
className={classNames.inputWrapper}
data-size={dataAttributes['data-size']}
>
{icon && (
<div
className="canon-TextFieldIcon"
data-size={responsiveSize}
className={classNames.icon}
data-size={dataAttributes['data-size']}
aria-hidden="true"
>
{icon}
</div>
)}
<Input
className="canon-TextFieldInput"
className={classNames.input}
{...(icon && { 'data-icon': true })}
placeholder={placeholder}
/>
@@ -17,68 +17,85 @@
import { forwardRef } from 'react';
import { Tooltip as TooltipPrimitive } from '@base-ui-components/react/tooltip';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
const TooltipTrigger = forwardRef<
React.ElementRef<typeof TooltipPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TooltipPrimitive.Trigger
ref={ref}
className={clsx('canon-TooltipTrigger', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Tooltip');
return (
<TooltipPrimitive.Trigger
ref={ref}
className={clsx(classNames.trigger, className)}
{...props}
/>
);
});
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
const TooltipPositioner = forwardRef<
React.ElementRef<typeof TooltipPrimitive.Positioner>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Positioner>
>(({ className, ...props }, ref) => (
<TooltipPrimitive.Positioner
ref={ref}
className={clsx('canon-TooltipPositioner', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Tooltip');
return (
<TooltipPrimitive.Positioner
ref={ref}
className={clsx(classNames.positioner, className)}
{...props}
/>
);
});
TooltipPositioner.displayName = TooltipPrimitive.Positioner.displayName;
const TooltipPopup = forwardRef<
React.ElementRef<typeof TooltipPrimitive.Popup>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Popup>
>(({ className, ...props }, ref) => (
<TooltipPrimitive.Popup
ref={ref}
className={clsx('canon-TooltipPopup', className)}
{...props}
/>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Tooltip');
return (
<TooltipPrimitive.Popup
ref={ref}
className={clsx(classNames.popup, className)}
{...props}
/>
);
});
TooltipPopup.displayName = TooltipPrimitive.Popup.displayName;
const TooltipArrow = forwardRef<
React.ElementRef<typeof TooltipPrimitive.Arrow>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Arrow>
>(({ className, ...props }, ref) => (
<TooltipPrimitive.Arrow
ref={ref}
className={clsx('canon-TooltipArrow', className)}
{...props}
>
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="canon-TooltipArrow-fill"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className="canon-TooltipArrow-outer-stroke"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="canon-TooltipArrow-inner-stroke"
/>
</svg>
</TooltipPrimitive.Arrow>
));
>(({ className, ...props }, ref) => {
const { classNames } = useStyles('Tooltip');
return (
<TooltipPrimitive.Arrow
ref={ref}
className={clsx(classNames.arrow, className)}
{...props}
>
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="canon-TooltipArrow-fill"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className="canon-TooltipArrow-outer-stroke"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="canon-TooltipArrow-inner-stroke"
/>
</svg>
</TooltipPrimitive.Arrow>
);
});
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName;
/** @public */
+92
View File
@@ -0,0 +1,92 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useBreakpoint, breakpoints } from './useBreakpoint';
import { componentDefinitions } from '../utils/componentDefinitions';
import type { ComponentDefinitionName, ComponentClassNames } from '../types';
/**
* Resolve a responsive value based on the current breakpoint
* @param value - The responsive value (string or object with breakpoint keys)
* @param breakpoint - The current breakpoint
* @returns The resolved value for the current breakpoint
*/
function resolveResponsiveValue(
value: string | Record<string, string>,
breakpoint: string,
): string | undefined {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'object' && value !== null) {
const index = breakpoints.findIndex(b => b.id === breakpoint);
// Look for value at current breakpoint or smaller
for (let i = index; i >= 0; i--) {
if (value[breakpoints[i].id]) {
return value[breakpoints[i].id];
}
}
// If no value found, check from smallest breakpoint up
for (let i = 0; i < breakpoints.length; i++) {
if (value[breakpoints[i].id]) {
return value[breakpoints[i].id];
}
}
}
return undefined;
}
/**
* React hook to get class names and data attributes for a component with responsive support
* @param componentName - The name of the component
* @param props - Object with prop values (can be responsive)
* @returns Object with classNames and dataAttributes
*/
export function useStyles<T extends ComponentDefinitionName>(
componentName: T,
props: Record<string, any> = {},
): {
classNames: ComponentClassNames<T>;
dataAttributes: Record<string, string>;
resolvedProps: Record<string, string>;
} {
const { breakpoint } = useBreakpoint();
const classNames = componentDefinitions[componentName]
.classNames as ComponentClassNames<T>;
// Resolve responsive values and generate data attributes
const dataAttributes: Record<string, string> = {};
const resolvedProps: Record<string, string> = {};
for (const [key, value] of Object.entries(props)) {
if (value !== undefined && value !== null) {
const resolvedValue = resolveResponsiveValue(value, breakpoint);
if (resolvedValue !== undefined) {
resolvedProps[key] = resolvedValue;
dataAttributes[`data-${key}`] = resolvedValue;
}
}
}
return {
classNames,
dataAttributes,
resolvedProps, // Also return resolved props for convenience
};
}
+3
View File
@@ -58,3 +58,6 @@ export * from './props';
// Hooks
export { useBreakpoint } from './hooks/useBreakpoint';
// Component Definitions
export * from './utils/componentDefinitions';
+42
View File
@@ -14,6 +14,8 @@
* limitations under the License.
*/
import { componentDefinitions } from './utils/componentDefinitions';
/** @public */
export type AsProps =
| 'div'
@@ -131,3 +133,43 @@ export interface UtilityProps extends SpaceProps {
justifyContent?: Responsive<JustifyContent>;
rowSpan?: Responsive<Columns | 'full'>;
}
/**
* Base type for the component styles structure
* @public
*/
export type ClassNamesMap = Record<string, string>;
/**
* Base type for the component styles structure
* @public
*/
export type DataAttributeValues = readonly (string | number | boolean)[];
/**
* Base type for the component styles structure
* @public
*/
export type DataAttributesMap = Record<string, DataAttributeValues>;
/**
* Base type for the component styles structure
* @public
*/
export interface ComponentDefinition {
classNames: ClassNamesMap;
dataAttributes?: DataAttributesMap;
}
/**
* Type utilities for extracting information from the component styles
* @public
*/
export type ComponentDefinitionName = keyof typeof componentDefinitions;
/**
* Helper type to extract class names for a component
* @public
*/
export type ComponentClassNames<T extends ComponentDefinitionName> =
(typeof componentDefinitions)[T]['classNames'];
@@ -0,0 +1,240 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ComponentDefinition } from '../types';
/**
* Component definitions for the Canon library
* @public
*/
export const componentDefinitions = {
Avatar: {
classNames: {
root: 'canon-AvatarRoot',
image: 'canon-AvatarImage',
fallback: 'canon-AvatarFallback',
},
dataAttributes: {
size: ['small', 'medium', 'large'] as const,
},
},
Box: {
classNames: {
root: 'canon-Box',
},
},
Button: {
classNames: {
root: 'canon-Button',
},
dataAttributes: {
size: ['small', 'medium', 'large'] as const,
variant: ['primary', 'secondary', 'ghost'] as const,
},
},
ButtonIcon: {
classNames: {
root: 'canon-ButtonIcon',
},
},
ButtonLink: {
classNames: {
root: 'canon-ButtonLink',
},
},
Checkbox: {
classNames: {
root: 'canon-CheckboxRoot',
label: 'canon-CheckboxLabel',
indicator: 'canon-CheckboxIndicator',
},
dataAttributes: {
checked: [true, false] as const,
},
},
Collapsible: {
classNames: {
root: 'canon-CollapsibleRoot',
trigger: 'canon-CollapsibleTrigger',
panel: 'canon-CollapsiblePanel',
},
},
Container: {
classNames: {
root: 'canon-Container',
},
},
FieldLabel: {
classNames: {
root: 'canon-FieldLabel',
label: 'canon-FieldLabelLabel',
secondaryLabel: 'canon-FieldLabelSecondaryLabel',
description: 'canon-FieldLabelDescription',
},
},
Flex: {
classNames: {
root: 'canon-Flex',
},
},
Grid: {
classNames: {
root: 'canon-Grid',
item: 'canon-GridItem',
},
},
Heading: {
classNames: {
root: 'canon-Heading',
},
dataAttributes: {
variant: ['title1', 'title2', 'title3', 'subtitle'] as const,
color: ['primary', 'secondary', 'muted'] as const,
truncate: [true, false] as const,
},
},
Icon: {
classNames: {
root: 'canon-Icon',
},
},
Link: {
classNames: {
root: 'canon-Link',
},
dataAttributes: {
variant: ['subtitle', 'body', 'caption', 'label'] as const,
weight: ['regular', 'bold'] as const,
},
},
Menu: {
classNames: {
trigger: 'canon-MenuTrigger',
backdrop: 'canon-MenuBackdrop',
positioner: 'canon-MenuPositioner',
popup: 'canon-MenuPopup',
arrow: 'canon-MenuArrow',
item: 'canon-MenuItem',
group: 'canon-MenuGroup',
groupLabel: 'canon-MenuGroupLabel',
radioGroup: 'canon-MenuRadioGroup',
radioItem: 'canon-MenuRadioItem',
radioItemIndicator: 'canon-MenuRadioItemIndicator',
checkboxItem: 'canon-MenuCheckboxItem',
checkboxItemIndicator: 'canon-MenuCheckboxItemIndicator',
submenuTrigger: 'canon-MenuSubmenuTrigger',
separator: 'canon-MenuSeparator',
},
},
RadioGroup: {
classNames: {
root: 'canon-RadioGroup',
content: 'canon-RadioGroupContent',
radio: 'canon-Radio',
},
},
ScrollArea: {
classNames: {
root: 'canon-ScrollAreaRoot',
viewport: 'canon-ScrollAreaViewport',
scrollbar: 'canon-ScrollAreaScrollbar',
thumb: 'canon-ScrollAreaThumb',
},
},
Select: {
classNames: {
root: 'canon-Select',
required: 'canon-SelectRequired',
trigger: 'canon-SelectTrigger',
value: 'canon-SelectValue',
icon: 'canon-SelectIcon',
popup: 'canon-SelectPopup',
item: 'canon-SelectItem',
itemIndicator: 'canon-SelectItemIndicator',
itemText: 'canon-SelectItemText',
description: 'canon-SelectDescription',
error: 'canon-SelectError',
},
dataAttributes: {
size: ['small', 'medium'] as const,
},
},
Switch: {
classNames: {
root: 'canon-Switch',
indicator: 'canon-SwitchIndicator',
},
},
Table: {
classNames: {
root: 'canon-TableRoot',
header: 'canon-TableHeader',
body: 'canon-TableBody',
row: 'canon-TableRow',
head: 'canon-TableHead',
caption: 'canon-TableCaption',
cell: 'canon-TableCell',
cellText: 'canon-TableCellText',
cellLink: 'canon-TableCellLink',
cellProfile: 'canon-TableCellProfile',
cellProfileAvatar: 'canon-TableCellProfileAvatar',
cellProfileAvatarImage: 'canon-TableCellProfileAvatarImage',
cellProfileAvatarFallback: 'canon-TableCellProfileAvatarFallback',
cellProfileName: 'canon-TableCellProfileName',
cellProfileLink: 'canon-TableCellProfileLink',
},
},
Tabs: {
classNames: {
root: 'canon-TabsRoot',
list: 'canon-TabsList',
indicator: 'canon-TabsIndicator',
tab: 'canon-TabsTab',
panel: 'canon-TabsPanel',
},
},
Text: {
classNames: {
root: 'canon-Text',
},
dataAttributes: {
variant: ['subtitle', 'body', 'caption', 'label'] as const,
weight: ['regular', 'bold'] as const,
color: ['primary', 'secondary', 'danger', 'warning', 'success'] as const,
truncate: [true, false] as const,
},
},
TextField: {
classNames: {
root: 'canon-TextField',
inputWrapper: 'canon-TextFieldInputWrapper',
icon: 'canon-TextFieldIcon',
input: 'canon-TextFieldInput',
},
dataAttributes: {
invalid: [true, false] as const,
disabled: [true, false] as const,
},
},
Tooltip: {
classNames: {
trigger: 'canon-TooltipTrigger',
positioner: 'canon-TooltipPositioner',
popup: 'canon-TooltipPopup',
arrow: 'canon-TooltipArrow',
},
},
} as const satisfies Record<string, ComponentDefinition>;