Merge pull request #31399 from backstage/bui-css-module

Convert component styles into CSS Modules
This commit is contained in:
Charles de Dreuille
2025-10-13 14:29:28 +01:00
committed by GitHub
81 changed files with 1089 additions and 2544 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': minor
---
**BREAKING** Restructure Backstage UI component styling to use CSS Modules instead of pure CSS. We don't expect this to be an issue in practice but it is important to call out that all styles are now loaded through CSS modules with generated class names. We are still providing fixed class names for all components to allow anyone to style their Backstage instance.
File diff suppressed because it is too large Load Diff
+13
View File
@@ -481,6 +481,11 @@ export const componentDefinitions: {
readonly footer: 'bui-DialogFooter';
};
};
readonly FieldError: {
readonly classNames: {
readonly root: 'bui-FieldError';
};
};
readonly FieldLabel: {
readonly classNames: {
readonly root: 'bui-FieldLabelWrapper';
@@ -700,6 +705,14 @@ export const componentDefinitions: {
readonly cellProfileLink: 'bui-TableCellProfileLink';
};
};
readonly TablePagination: {
readonly classNames: {
readonly root: 'bui-TablePagination';
readonly left: 'bui-TablePaginationLeft';
readonly right: 'bui-TablePaginationRight';
readonly select: 'bui-TablePaginationSelect';
};
};
readonly Tabs: {
readonly classNames: {
readonly tabs: 'bui-Tabs';
+9 -3
View File
@@ -19,6 +19,7 @@ import { Avatar as AvatarPrimitive } from '@base-ui-components/react/avatar';
import clsx from 'clsx';
import { AvatarProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './Avatar.module.css';
/** @public */
export const Avatar = forwardRef<
@@ -33,12 +34,17 @@ export const Avatar = forwardRef<
return (
<AvatarPrimitive.Root
ref={ref}
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
data-size={size}
{...rest}
>
<AvatarPrimitive.Image className={classNames.image} src={src} />
<AvatarPrimitive.Fallback className={classNames.fallback}>
<AvatarPrimitive.Image
className={clsx(classNames.image, styles[classNames.image])}
src={src}
/>
<AvatarPrimitive.Fallback
className={clsx(classNames.fallback, styles[classNames.fallback])}
>
{(name || '')
.split(' ')
.map(word => word[0])
+7 -1
View File
@@ -18,6 +18,7 @@ import { createElement, forwardRef } from 'react';
import { BoxProps } from './types';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './Box.module.css';
/** @public */
export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
@@ -32,7 +33,12 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
as,
{
ref,
className: clsx(classNames.root, utilityClasses, className),
className: clsx(
classNames.root,
styles[classNames.root],
utilityClasses,
className,
),
style,
...rest,
},
+2 -1
View File
@@ -19,6 +19,7 @@ import { forwardRef, Ref } from 'react';
import { Button as RAButton } from 'react-aria-components';
import type { ButtonProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './Button.module.css';
/** @public */
export const Button = forwardRef(
@@ -33,7 +34,7 @@ export const Button = forwardRef(
return (
<RAButton
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
ref={ref}
{...dataAttributes}
{...rest}
@@ -19,6 +19,8 @@ import { forwardRef, Ref } from 'react';
import { Button as RAButton } from 'react-aria-components';
import type { ButtonIconProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import stylesButtonIcon from './ButtonIcon.module.css';
import stylesButton from '../Button/Button.module.css';
/** @public */
export const ButtonIcon = forwardRef(
@@ -35,7 +37,13 @@ export const ButtonIcon = forwardRef(
return (
<RAButton
className={clsx(classNames.root, classNamesButtonIcon.root, className)}
className={clsx(
classNames.root,
classNamesButtonIcon.root,
stylesButton[classNames.root],
stylesButtonIcon[classNamesButtonIcon.root],
className,
)}
ref={ref}
{...dataAttributes}
{...rest}
@@ -21,6 +21,7 @@ import { useNavigate, useHref } from 'react-router-dom';
import type { ButtonLinkProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import { isExternalLink } from '../../utils/isExternalLink';
import stylesButton from '../Button/Button.module.css';
/** @public */
export const ButtonLink = forwardRef(
@@ -47,6 +48,7 @@ export const ButtonLink = forwardRef(
className={clsx(
classNames.root,
classNamesButtonLink.root,
stylesButton[classNames.root],
className,
)}
ref={ref}
@@ -68,6 +70,7 @@ export const ButtonLink = forwardRef(
className={clsx(
classNames.root,
classNamesButtonLink.root,
stylesButton[classNames.root],
className,
)}
ref={ref}
+25 -4
View File
@@ -24,6 +24,7 @@ import type {
CardBodyProps,
CardFooterProps,
} from './types';
import styles from './Card.module.css';
/**
* Card component.
@@ -35,7 +36,11 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
const { classNames } = useStyles('Card');
return (
<div ref={ref} className={clsx(classNames.root, className)} {...rest} />
<div
ref={ref}
className={clsx(classNames.root, styles[classNames.root], className)}
{...rest}
/>
);
});
@@ -50,7 +55,15 @@ export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
const { classNames } = useStyles('Card');
return (
<div ref={ref} className={clsx(classNames.header, className)} {...rest} />
<div
ref={ref}
className={clsx(
classNames.header,
styles[classNames.header],
className,
)}
{...rest}
/>
);
},
);
@@ -68,7 +81,7 @@ export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
return (
<ScrollArea.Root
ref={ref}
className={clsx(classNames.body, className)}
className={clsx(classNames.body, styles[classNames.body], className)}
{...rest}
>
<ScrollArea.Viewport style={{ paddingInline: 'var(--bui-space-3)' }}>
@@ -96,7 +109,15 @@ export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
const { classNames } = useStyles('Card');
return (
<div ref={ref} className={clsx(classNames.footer, className)} {...rest} />
<div
ref={ref}
className={clsx(
classNames.footer,
styles[classNames.footer],
className,
)}
{...rest}
/>
);
},
);
@@ -20,6 +20,7 @@ import { Icon } from '../..';
import type { CheckboxProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import clsx from 'clsx';
import styles from './Checkbox.module.css';
/** @public */
export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
@@ -41,7 +42,7 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
const checkboxElement = (
<CheckboxPrimitive.Root
ref={ref}
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
checked={checked}
onCheckedChange={onChange}
disabled={disabled}
@@ -50,14 +51,16 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
value={value}
style={style}
>
<CheckboxPrimitive.Indicator className={classNames.indicator}>
<CheckboxPrimitive.Indicator
className={clsx(classNames.indicator, styles[classNames.indicator])}
>
<Icon name="check" size={12} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
return label ? (
<label className={classNames.label}>
<label className={clsx(classNames.label, styles[classNames.label])}>
{checkboxElement}
{label}
</label>
@@ -18,6 +18,7 @@ import { forwardRef } from 'react';
import { Collapsible as CollapsiblePrimitive } from '@base-ui-components/react/collapsible';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './Collapsible.module.css';
const CollapsibleRoot = forwardRef<
React.ElementRef<typeof CollapsiblePrimitive.Root>,
@@ -28,7 +29,7 @@ const CollapsibleRoot = forwardRef<
return (
<CollapsiblePrimitive.Root
ref={ref}
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
{...props}
/>
);
@@ -44,7 +45,11 @@ const CollapsibleTrigger = forwardRef<
return (
<CollapsiblePrimitive.Trigger
ref={ref}
className={clsx(classNames.trigger, className)}
className={clsx(
classNames.trigger,
styles[classNames.trigger],
className,
)}
{...props}
/>
);
@@ -60,7 +65,7 @@ const CollapsiblePanel = forwardRef<
return (
<CollapsiblePrimitive.Panel
ref={ref}
className={clsx(classNames.panel, className)}
className={clsx(classNames.panel, styles[classNames.panel], className)}
{...props}
/>
);
@@ -18,6 +18,7 @@ import { forwardRef } from 'react';
import { ContainerProps } from './types';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './Container.module.css';
/** @public */
export const Container = forwardRef<HTMLDivElement, ContainerProps>(
@@ -32,7 +33,12 @@ export const Container = forwardRef<HTMLDivElement, ContainerProps>(
return (
<div
ref={ref}
className={clsx(classNames.root, utilityClasses, className)}
className={clsx(
classNames.root,
utilityClasses,
styles[classNames.root],
className,
)}
style={style}
{...rest}
/>
@@ -0,0 +1,139 @@
/*
* 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.
*/
@layer components {
.bui-DialogOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(232, 232, 232, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
[data-theme='dark'] .bui-Dialog {
background: rgba(0, 0, 0, 0.5);
}
.bui-DialogOverlay[data-entering] {
animation: fade-in 200ms ease-out forwards;
}
.bui-DialogOverlay[data-exiting] {
animation: fade-out 150ms ease-out forwards;
}
.bui-Dialog {
background: var(--bui-bg-surface-1);
border-radius: 0.5rem;
border: 1px solid var(--bui-border);
color: var(--bui-fg-primary);
position: relative;
width: min(var(--bui-dialog-min-width, 400px), calc(100vw - 3rem));
max-width: calc(100vw - 3rem);
height: min(var(--bui-dialog-min-height, auto), calc(100vh - 3rem));
max-height: calc(100vh - 3rem);
display: flex;
flex-direction: column;
outline: none;
}
/* Dialog entering animation */
.bui-DialogOverlay[data-entering] .bui-Dialog {
animation: dialog-enter 150ms ease-out forwards;
}
/* Dialog exiting animation */
.bui-DialogOverlay[data-exiting] .bui-Dialog {
animation: dialog-exit 150ms ease-out forwards;
}
.bui-DialogHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding-inline: var(--bui-space-3);
padding-block: var(--bui-space-2);
border-bottom: 1px solid var(--bui-border);
}
.bui-DialogHeaderTitle {
font-size: var(--bui-font-size-3);
font-weight: var(--bui-font-weight-bold);
margin: 0;
}
.bui-DialogFooter {
display: flex;
align-items: center;
justify-content: end;
gap: var(--bui-space-2);
padding-inline: var(--bui-space-3);
padding-block: var(--bui-space-3);
border-top: 1px solid var(--bui-border);
}
.bui-DialogBody {
padding: var(--bui-space-3);
flex: 1;
overflow-y: auto;
}
/* Keyframe animations */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes dialog-enter {
from {
opacity: 0.5;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dialog-exit {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
}
@@ -1,122 +0,0 @@
/* Backdrop */
.bui-DialogOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(232, 232, 232, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
[data-theme='dark'] .bui-Dialog {
background: rgba(0, 0, 0, 0.5);
}
.bui-DialogOverlay[data-entering] {
animation: fade-in 200ms ease-out forwards;
}
.bui-DialogOverlay[data-exiting] {
animation: fade-out 150ms ease-out forwards;
}
.bui-Dialog {
background: var(--bui-bg-surface-1);
border-radius: 0.5rem;
border: 1px solid var(--bui-border);
color: var(--bui-fg-primary);
position: relative;
width: min(var(--bui-dialog-min-width, 400px), calc(100vw - 3rem));
max-width: calc(100vw - 3rem);
height: min(var(--bui-dialog-min-height, auto), calc(100vh - 3rem));
max-height: calc(100vh - 3rem);
display: flex;
flex-direction: column;
outline: none;
}
/* Dialog entering animation */
.bui-DialogOverlay[data-entering] .bui-Dialog {
animation: dialog-enter 150ms ease-out forwards;
}
/* Dialog exiting animation */
.bui-DialogOverlay[data-exiting] .bui-Dialog {
animation: dialog-exit 150ms ease-out forwards;
}
.bui-DialogHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding-inline: var(--bui-space-3);
padding-block: var(--bui-space-2);
border-bottom: 1px solid var(--bui-border);
}
.bui-DialogHeaderTitle {
font-size: var(--bui-font-size-3);
font-weight: var(--bui-font-weight-bold);
margin: 0;
}
.bui-DialogFooter {
display: flex;
align-items: center;
justify-content: end;
gap: var(--bui-space-2);
padding-inline: var(--bui-space-3);
padding-block: var(--bui-space-3);
border-top: 1px solid var(--bui-border);
}
.bui-DialogBody {
padding: var(--bui-space-3);
flex: 1;
overflow-y: auto;
}
/* Keyframe animations */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes dialog-enter {
from {
opacity: 0.5;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dialog-exit {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
+26 -7
View File
@@ -28,11 +28,11 @@ import type {
DialogProps,
DialogBodyProps,
} from './types';
import './Dialog.styles.css';
import { RiCloseLine } from '@remixicon/react';
import { Button } from '../Button';
import { useStyles } from '../../hooks/useStyles';
import { Flex } from '../Flex';
import styles from './Dialog.module.css';
/** @public */
export const DialogTrigger = (props: DialogTriggerProps) => {
@@ -47,13 +47,17 @@ export const Dialog = forwardRef<React.ElementRef<typeof Modal>, DialogProps>(
return (
<Modal
ref={ref}
className={clsx(classNames.overlay)}
className={clsx(classNames.overlay, styles[classNames.overlay])}
isDismissable
isKeyboardDismissDisabled={false}
{...props}
>
<RADialog
className={clsx(classNames.dialog, className)}
className={clsx(
classNames.dialog,
styles[classNames.dialog],
className,
)}
style={{
['--bui-dialog-min-width' as keyof React.CSSProperties]:
typeof width === 'number' ? `${width}px` : width || '400px',
@@ -82,8 +86,15 @@ export const DialogHeader = forwardRef<
const { classNames } = useStyles('Dialog');
return (
<Flex ref={ref} className={clsx(classNames.header, className)} {...props}>
<Heading slot="title" className={classNames.headerTitle}>
<Flex
ref={ref}
className={clsx(classNames.header, styles[classNames.header], className)}
{...props}
>
<Heading
slot="title"
className={clsx(classNames.headerTitle, styles[classNames.headerTitle])}
>
{children}
</Heading>
<Button name="close" aria-label="Close" variant="tertiary" slot="close">
@@ -100,7 +111,11 @@ export const DialogBody = forwardRef<React.ElementRef<'div'>, DialogBodyProps>(
const { classNames } = useStyles('Dialog');
return (
<div className={clsx(classNames.body, className)} ref={ref} {...props}>
<div
className={clsx(classNames.body, styles[classNames.body], className)}
ref={ref}
{...props}
>
{children}
</div>
);
@@ -117,7 +132,11 @@ export const DialogFooter = forwardRef<
const { classNames } = useStyles('Dialog');
return (
<div ref={ref} className={clsx(classNames.footer, className)} {...props}>
<div
ref={ref}
className={clsx(classNames.footer, styles[classNames.footer], className)}
{...props}
>
{children}
</div>
);
@@ -20,15 +20,19 @@ import {
type FieldErrorProps,
} from 'react-aria-components';
import clsx from 'clsx';
import styles from './FieldError.module.css';
import { useStyles } from '../../hooks/useStyles';
/** @public */
export const FieldError = forwardRef<HTMLDivElement, FieldErrorProps>(
(props: FieldErrorProps, ref) => {
const { className, ...rest } = props;
const { classNames } = useStyles('FieldError');
return (
<AriaFieldError
className={clsx('bui-FieldError', className)}
className={clsx(classNames.root, styles[classNames.root], className)}
ref={ref}
{...rest}
/>
@@ -17,6 +17,8 @@ import { Label } from 'react-aria-components';
import { forwardRef } from 'react';
import type { FieldLabelProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './FieldLabel.module.css';
import clsx from 'clsx';
/** @public */
export const FieldLabel = forwardRef<HTMLDivElement, FieldLabelProps>(
@@ -28,19 +30,40 @@ export const FieldLabel = forwardRef<HTMLDivElement, FieldLabelProps>(
if (!label) return null;
return (
<div className={classNames.root} {...rest} ref={ref}>
<div
className={clsx(classNames.root, styles[classNames.root])}
{...rest}
ref={ref}
>
{label && (
<Label className={classNames.label} htmlFor={htmlFor} id={id}>
<Label
className={clsx(classNames.label, styles[classNames.label])}
htmlFor={htmlFor}
id={id}
>
{label}
{secondaryLabel && (
<span aria-hidden="true" className={classNames.secondaryLabel}>
<span
aria-hidden="true"
className={clsx(
classNames.secondaryLabel,
styles[classNames.secondaryLabel],
)}
>
({secondaryLabel})
</span>
)}
</Label>
)}
{description && (
<div className={classNames.description}>{description}</div>
<div
className={clsx(
classNames.description,
styles[classNames.description],
)}
>
{description}
</div>
)}
</div>
);
+7 -1
View File
@@ -18,6 +18,7 @@ import { forwardRef } from 'react';
import { FlexProps } from './types';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './Flex.module.css';
/** @public */
export const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
@@ -31,7 +32,12 @@ export const Flex = forwardRef<HTMLDivElement, FlexProps>((props, ref) => {
return (
<div
ref={ref}
className={clsx(classNames.root, utilityClasses, className)}
className={clsx(
classNames.root,
utilityClasses,
styles[classNames.root],
className,
)}
style={style}
{...rest}
/>
+13 -2
View File
@@ -18,6 +18,7 @@ import { forwardRef } from 'react';
import clsx from 'clsx';
import type { GridItemProps, GridProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './Grid.module.css';
const GridRoot = forwardRef<HTMLDivElement, GridProps>((props, ref) => {
const { classNames, utilityClasses, style, cleanedProps } = useStyles(
@@ -30,7 +31,12 @@ const GridRoot = forwardRef<HTMLDivElement, GridProps>((props, ref) => {
return (
<div
ref={ref}
className={clsx(classNames.root, utilityClasses, className)}
className={clsx(
classNames.root,
utilityClasses,
styles[classNames.root],
className,
)}
style={style}
{...rest}
/>
@@ -48,7 +54,12 @@ const GridItem = forwardRef<HTMLDivElement, GridItemProps>((props, ref) => {
return (
<div
ref={ref}
className={clsx(classNames.root, utilityClasses, className)}
className={clsx(
classNames.root,
utilityClasses,
styles[classNames.root],
className,
)}
style={style}
{...rest}
/>
+8 -1
View File
@@ -19,6 +19,8 @@ import { HeaderToolbar } from './HeaderToolbar';
import { Tabs, TabList, Tab } from '../Tabs';
import { useStyles } from '../../hooks/useStyles';
import { type NavigateOptions } from 'react-router-dom';
import styles from './Header.module.css';
import clsx from 'clsx';
declare module 'react-aria-components' {
interface RouterConfig {
@@ -49,7 +51,12 @@ export const Header = (props: HeaderProps) => {
hasTabs={hasTabs}
/>
{tabs && (
<div className={classNames.tabsWrapper}>
<div
className={clsx(
classNames.tabsWrapper,
styles[classNames.tabsWrapper],
)}
>
<Tabs onSelectionChange={onTabSelectionChange}>
<TabList>
{tabs?.map(tab => (
@@ -21,6 +21,8 @@ import { RiShapesLine } from '@remixicon/react';
import type { HeaderToolbarProps } from './types';
import { Text } from '../Text';
import { useNavigate, useHref } from 'react-router-dom';
import styles from './Header.module.css';
import clsx from 'clsx';
/**
* A component that renders a toolbar.
@@ -39,27 +41,65 @@ export const HeaderToolbar = (props: HeaderToolbarProps) => {
const titleContent = (
<>
<div className={classNames.toolbarIcon}>{icon || <RiShapesLine />}</div>
<div
className={clsx(classNames.toolbarIcon, styles[classNames.toolbarIcon])}
>
{icon || <RiShapesLine />}
</div>
<Text variant="body-medium">{title || 'Your plugin'}</Text>
</>
);
return (
<RouterProvider navigate={navigate} useHref={useHref}>
<div className={classNames.toolbar} data-has-tabs={hasTabs}>
<div className={classNames.toolbarWrapper} ref={toolbarWrapperRef}>
<div className={classNames.toolbarContent} ref={toolbarContentRef}>
<div
className={clsx(classNames.toolbar, styles[classNames.toolbar])}
data-has-tabs={hasTabs}
>
<div
className={clsx(
classNames.toolbarWrapper,
styles[classNames.toolbarWrapper],
)}
ref={toolbarWrapperRef}
>
<div
className={clsx(
classNames.toolbarContent,
styles[classNames.toolbarContent],
)}
ref={toolbarContentRef}
>
<Text as="h1" variant="body-medium">
{titleLink ? (
<Link className={classNames.toolbarName} href={titleLink}>
<Link
className={clsx(
classNames.toolbarName,
styles[classNames.toolbarName],
)}
href={titleLink}
>
{titleContent}
</Link>
) : (
<div className={classNames.toolbarName}>{titleContent}</div>
<div
className={clsx(
classNames.toolbarName,
styles[classNames.toolbarName],
)}
>
{titleContent}
</div>
)}
</Text>
</div>
<div className={classNames.toolbarControls} ref={toolbarControlsRef}>
<div
className={clsx(
classNames.toolbarControls,
styles[classNames.toolbarControls],
)}
ref={toolbarControlsRef}
>
{customActions}
</div>
</div>
@@ -22,6 +22,8 @@ import { useStyles } from '../../hooks/useStyles';
import { Container } from '../Container';
import { Link } from '../Link';
import { Fragment } from 'react/jsx-runtime';
import styles from './HeaderPage.module.css';
import clsx from 'clsx';
/**
* A component that renders a header page.
@@ -33,9 +35,14 @@ export const HeaderPage = (props: HeaderPageProps) => {
const { classNames } = useStyles('HeaderPage');
return (
<Container className={classNames.root}>
<div className={classNames.content}>
<div className={classNames.breadcrumbs}>
<Container className={clsx(classNames.root, styles[classNames.root])}>
<div className={clsx(classNames.content, styles[classNames.content])}>
<div
className={clsx(
classNames.breadcrumbs,
styles[classNames.breadcrumbs],
)}
>
{breadcrumbs &&
breadcrumbs.map(breadcrumb => (
<Fragment key={breadcrumb.label}>
@@ -56,10 +63,17 @@ export const HeaderPage = (props: HeaderPageProps) => {
{title}
</Text>
</div>
<div className={classNames.controls}>{customActions}</div>
<div className={clsx(classNames.controls, styles[classNames.controls])}>
{customActions}
</div>
</div>
{tabs && (
<div className={classNames.tabsWrapper}>
<div
className={clsx(
classNames.tabsWrapper,
styles[classNames.tabsWrapper],
)}
>
<Tabs>
<TabList>
{tabs.map(tab => (
+2 -1
View File
@@ -19,6 +19,7 @@ import { useIcons } from './context';
import type { IconProps } from './types';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './Icon.module.css';
/** @public */
export const Icon = (props: IconProps) => {
@@ -36,7 +37,7 @@ export const Icon = (props: IconProps) => {
return (
<BckstageIcon
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
style={{
...(size ? { width: size, height: size } : {}),
...style,
+16 -2
View File
@@ -21,6 +21,8 @@ import { useStyles } from '../../hooks/useStyles';
import type { LinkProps } from './types';
import { useNavigate, useHref } from 'react-router-dom';
import { isExternalLink } from '../../utils/isExternalLink';
import stylesLink from './Link.module.css';
import stylesText from '../Text/Text.module.css';
/** @public */
export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
@@ -50,7 +52,13 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
return (
<AriaLink
ref={ref}
className={clsx(textClassNames.root, linkClassNames.root, className)}
className={clsx(
textClassNames.root,
linkClassNames.root,
stylesText[textClassNames.root],
stylesLink[linkClassNames.root],
className,
)}
data-truncate={truncate}
href={href}
{...textDataAttributes}
@@ -64,7 +72,13 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
<RouterProvider navigate={navigate} useHref={useHref}>
<AriaLink
ref={ref}
className={clsx(textClassNames.root, linkClassNames.root, className)}
className={clsx(
textClassNames.root,
linkClassNames.root,
stylesText[textClassNames.root],
stylesLink[linkClassNames.root],
className,
)}
data-truncate={truncate}
{...textDataAttributes}
href={href}
@@ -0,0 +1,268 @@
/*
* 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.
*/
@layer components {
.bui-MenuPopover {
display: flex;
flex-direction: column;
border: 1px solid var(--bui-border);
border-radius: var(--bui-radius-2);
background: var(--bui-bg-surface-1);
color: var(--bui-fg-primary);
outline: none;
transition: transform 200ms, opacity 200ms;
/* Let React Aria handle height constraints naturally */
min-height: 0;
/* Remove overflow from popover since ScrollArea will handle it */
overflow: hidden;
&[data-entering],
&[data-exiting] {
transform: var(--origin);
opacity: 0;
}
&[data-placement='top'] {
--origin: translateY(8px);
}
&[data-placement='bottom'] {
--origin: translateY(-8px);
}
&[data-placement='right'] {
--origin: translateX(-8px);
}
&[data-placement='left'] {
--origin: translateX(8px);
}
}
.bui-MenuContent {
max-height: inherit;
box-sizing: border-box;
overflow: auto;
min-width: 150px;
box-sizing: border-box;
outline: none;
padding-block: var(--bui-space-1);
}
/* Ensure ScrollArea works properly within MenuRA popover */
.bui-MenuPopover .bui-ScrollAreaRoot {
/* Take full height of popover */
height: 100%;
min-height: 0;
flex: 1;
/* Ensure the root container has proper height constraints */
display: flex;
flex-direction: column;
}
.bui-MenuPopover .bui-ScrollAreaScrollbar {
margin-inline: var(--bui-space-1_5);
}
.bui-MenuItem {
padding-inline: var(--bui-space-1);
display: block;
&[data-focused] .bui-MenuItemWrapper {
background: var(--bui-bg-surface-2);
color: var(--bui-fg-primary);
}
&[data-open] .bui-MenuItemWrapper {
background: var(--bui-bg-surface-2);
color: var(--bui-fg-primary);
}
&[data-color='danger'] .bui-MenuItemWrapper {
color: var(--bui-fg-danger);
}
&[data-color='danger'][data-focused] .bui-MenuItemWrapper {
background: var(--bui-bg-danger);
color: var(--bui-fg-danger);
}
&[data-has-submenu] {
& > .bui-MenuItemWrapper > .bui-MenuItemArrow {
display: block;
}
}
}
.bui-MenuItemWrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 2rem;
padding-inline: var(--bui-space-2);
border-radius: var(--bui-radius-2);
outline: none;
cursor: default;
color: var(--bui-fg-primary);
font-size: var(--bui-font-size-3);
gap: var(--bui-space-6);
}
.bui-MenuItemListBox {
padding-inline: var(--bui-space-1);
display: block;
&:hover .bui-MenuItemWrapper {
background: var(--bui-bg-surface-2);
color: var(--bui-fg-primary);
}
&[data-selected] .bui-MenuItemListBoxCheck {
& > svg {
opacity: 1;
color: var(--bui-fg-primary);
}
}
}
.bui-MenuItemListBoxCheck {
display: flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
& > svg {
opacity: 0;
width: 1rem;
height: 1rem;
}
}
.bui-MenuItemContent {
display: flex;
align-items: center;
gap: var(--bui-space-2);
& > svg {
width: 1rem;
height: 1rem;
}
}
.bui-MenuItemArrow {
display: none;
width: 1rem;
height: 1rem;
& > svg {
width: 1rem;
height: 1rem;
}
}
.bui-MenuSection {
&:first-child .bui-MenuSectionHeader {
padding-top: 0;
}
}
.bui-MenuSectionHeader {
height: 2rem;
display: flex;
align-items: center;
padding-top: var(--bui-space-3);
padding-left: var(--bui-space-3);
color: var(--bui-fg-primary);
font-size: var(--bui-font-size-1);
font-weight: bold;
letter-spacing: 0.05rem;
text-transform: uppercase;
}
.bui-MenuSeparator {
height: 1px;
background: var(--bui-border);
margin-inline: var(--bui-space-1_5);
margin-block: var(--bui-space-1);
}
.bui-MenuSearchField {
position: relative;
font-family: var(--bui-font-regular);
width: 100%;
flex-shrink: 0;
&[data-empty] {
.bui-MenuSearchFieldClear {
display: none;
}
}
}
.bui-MenuSearchFieldInput {
display: flex;
align-items: center;
padding: 0 var(--bui-space-3);
border: none;
border-bottom: 1px solid var(--bui-border);
background-color: var(--bui-bg-surface-1);
font-size: var(--bui-font-size-3);
font-family: var(--bui-font-regular);
font-weight: var(--bui-font-weight-regular);
color: var(--bui-fg-primary);
width: 100%;
height: 2rem;
cursor: inherit;
outline: none;
&::-webkit-search-cancel-button,
&::-webkit-search-decoration {
-webkit-appearance: none;
}
}
.bui-MenuSearchFieldClear {
position: absolute;
right: var(--bui-space-2);
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
color: var(--bui-fg-secondary);
transition: color 0.2s ease-in-out;
& > svg {
width: 1rem;
height: 1rem;
}
}
.bui-MenuEmptyState {
padding-inline: var(--bui-space-3);
padding-block: var(--bui-space-2);
color: var(--bui-fg-secondary);
font-size: var(--bui-font-size-3);
font-family: var(--bui-font-regular);
font-weight: var(--bui-font-weight-regular);
}
}
@@ -1,266 +0,0 @@
/*
* 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.
*/
.bui-MenuPopover {
display: flex;
flex-direction: column;
border: 1px solid var(--bui-border);
border-radius: var(--bui-radius-2);
background: var(--bui-bg-surface-1);
color: var(--bui-fg-primary);
outline: none;
transition: transform 200ms, opacity 200ms;
/* Let React Aria handle height constraints naturally */
min-height: 0;
/* Remove overflow from popover since ScrollArea will handle it */
overflow: hidden;
&[data-entering],
&[data-exiting] {
transform: var(--origin);
opacity: 0;
}
&[data-placement='top'] {
--origin: translateY(8px);
}
&[data-placement='bottom'] {
--origin: translateY(-8px);
}
&[data-placement='right'] {
--origin: translateX(-8px);
}
&[data-placement='left'] {
--origin: translateX(8px);
}
}
.bui-MenuContent {
max-height: inherit;
box-sizing: border-box;
overflow: auto;
min-width: 150px;
box-sizing: border-box;
outline: none;
padding-block: var(--bui-space-1);
}
/* Ensure ScrollArea works properly within MenuRA popover */
.bui-MenuPopover .bui-ScrollAreaRoot {
/* Take full height of popover */
height: 100%;
min-height: 0;
flex: 1;
/* Ensure the root container has proper height constraints */
display: flex;
flex-direction: column;
}
.bui-MenuPopover .bui-ScrollAreaScrollbar {
margin-inline: var(--bui-space-1_5);
}
.bui-MenuItem {
padding-inline: var(--bui-space-1);
display: block;
&[data-focused] .bui-MenuItemWrapper {
background: var(--bui-bg-surface-2);
color: var(--bui-fg-primary);
}
&[data-open] .bui-MenuItemWrapper {
background: var(--bui-bg-surface-2);
color: var(--bui-fg-primary);
}
&[data-color='danger'] .bui-MenuItemWrapper {
color: var(--bui-fg-danger);
}
&[data-color='danger'][data-focused] .bui-MenuItemWrapper {
background: var(--bui-bg-danger);
color: var(--bui-fg-danger);
}
&[data-has-submenu] {
& > .bui-MenuItemWrapper > .bui-MenuItemArrow {
display: block;
}
}
}
.bui-MenuItemWrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 2rem;
padding-inline: var(--bui-space-2);
border-radius: var(--bui-radius-2);
outline: none;
cursor: default;
color: var(--bui-fg-primary);
font-size: var(--bui-font-size-3);
gap: var(--bui-space-6);
}
.bui-MenuItemListBox {
padding-inline: var(--bui-space-1);
display: block;
&:hover .bui-MenuItemWrapper {
background: var(--bui-bg-surface-2);
color: var(--bui-fg-primary);
}
&[data-selected] .bui-MenuItemListBoxCheck {
& > svg {
opacity: 1;
color: var(--bui-fg-primary);
}
}
}
.bui-MenuItemListBoxCheck {
display: flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
& > svg {
opacity: 0;
width: 1rem;
height: 1rem;
}
}
.bui-MenuItemContent {
display: flex;
align-items: center;
gap: var(--bui-space-2);
& > svg {
width: 1rem;
height: 1rem;
}
}
.bui-MenuItemArrow {
display: none;
width: 1rem;
height: 1rem;
& > svg {
width: 1rem;
height: 1rem;
}
}
.bui-MenuSection {
&:first-child .bui-MenuSectionHeader {
padding-top: 0;
}
}
.bui-MenuSectionHeader {
height: 2rem;
display: flex;
align-items: center;
padding-top: var(--bui-space-3);
padding-left: var(--bui-space-3);
color: var(--bui-fg-primary);
font-size: var(--bui-font-size-1);
font-weight: bold;
letter-spacing: 0.05rem;
text-transform: uppercase;
}
.bui-MenuSeparator {
height: 1px;
background: var(--bui-border);
margin-inline: var(--bui-space-1_5);
margin-block: var(--bui-space-1);
}
.bui-MenuSearchField {
position: relative;
font-family: var(--bui-font-regular);
width: 100%;
flex-shrink: 0;
&[data-empty] {
.bui-MenuSearchFieldClear {
display: none;
}
}
}
.bui-MenuSearchFieldInput {
display: flex;
align-items: center;
padding: 0 var(--bui-space-3);
border: none;
border-bottom: 1px solid var(--bui-border);
background-color: var(--bui-bg-surface-1);
font-size: var(--bui-font-size-3);
font-family: var(--bui-font-regular);
font-weight: var(--bui-font-weight-regular);
color: var(--bui-fg-primary);
width: 100%;
height: 2rem;
cursor: inherit;
outline: none;
&::-webkit-search-cancel-button,
&::-webkit-search-decoration {
-webkit-appearance: none;
}
}
.bui-MenuSearchFieldClear {
position: absolute;
right: var(--bui-space-2);
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
color: var(--bui-fg-secondary);
transition: color 0.2s ease-in-out;
& > svg {
width: 1rem;
height: 1rem;
}
}
.bui-MenuEmptyState {
padding-inline: var(--bui-space-3);
padding-block: var(--bui-space-2);
color: var(--bui-fg-secondary);
font-size: var(--bui-font-size-3);
font-family: var(--bui-font-regular);
font-weight: var(--bui-font-weight-regular);
}
+117 -30
View File
@@ -56,6 +56,8 @@ import {
import { useNavigate, useHref } from 'react-router-dom';
import { isExternalLink } from '../../utils/isExternalLink';
import { useRef, useEffect, useContext } from 'react';
import styles from './Menu.module.css';
import clsx from 'clsx';
// The height will be used for virtualized menus. It should match the size set in CSS for each menu item.
const rowHeight = 32;
@@ -63,7 +65,11 @@ const rowHeight = 32;
const MenuEmptyState = () => {
const { classNames } = useStyles('Menu');
return <div className={classNames.emptyState}>No results found.</div>;
return (
<div className={clsx(classNames.emptyState, styles[classNames.emptyState])}>
No results found.
</div>
);
};
/** @public */
@@ -118,7 +124,7 @@ export const Menu = (props: MenuProps<object>) => {
const menuContent = (
<RAMenu
className={classNames.content}
className={clsx(classNames.content, styles[classNames.content])}
renderEmptyState={() => <MenuEmptyState />}
style={{ width: newMaxWidth, maxHeight }}
{...rest}
@@ -128,7 +134,7 @@ export const Menu = (props: MenuProps<object>) => {
return (
<RAPopover
ref={popoverRef}
className={classNames.popover}
className={clsx(classNames.popover, styles[classNames.popover])}
placement={placement}
isNonModal={true}
isKeyboardDismissDisabled={false}
@@ -166,7 +172,7 @@ export const MenuListBox = (props: MenuListBoxProps<object>) => {
const listBoxContent = (
<RAListBox
className={classNames.content}
className={clsx(classNames.content, styles[classNames.content])}
selectionMode={selectionMode}
style={{ width: newMaxWidth, maxHeight }}
{...rest}
@@ -174,7 +180,10 @@ export const MenuListBox = (props: MenuListBoxProps<object>) => {
);
return (
<RAPopover className={classNames.popover} placement={placement}>
<RAPopover
className={clsx(classNames.popover, styles[classNames.popover])}
placement={placement}
>
{virtualized ? (
<Virtualizer
layout={ListLayout}
@@ -207,7 +216,7 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps<object>) => {
const menuContent = (
<RAMenu
className={classNames.content}
className={clsx(classNames.content, styles[classNames.content])}
renderEmptyState={() => <MenuEmptyState />}
style={{ width: newMaxWidth, maxHeight }}
{...rest}
@@ -215,16 +224,32 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps<object>) => {
);
return (
<RAPopover className={classNames.popover} placement={placement}>
<RAPopover
className={clsx(classNames.popover, styles[classNames.popover])}
placement={placement}
>
<RouterProvider navigate={navigate} useHref={useHref}>
<RAAutocomplete filter={contains}>
<RASearchField className={classNames.searchField}>
<RASearchField
className={clsx(
classNames.searchField,
styles[classNames.searchField],
)}
>
<RAInput
className={classNames.searchFieldInput}
className={clsx(
classNames.searchFieldInput,
styles[classNames.searchFieldInput],
)}
aria-label="Search"
placeholder={props.placeholder || 'Search...'}
/>
<RAButton className={classNames.searchFieldClear}>
<RAButton
className={clsx(
classNames.searchFieldClear,
styles[classNames.searchFieldClear],
)}
>
<RiCloseCircleLine />
</RAButton>
</RASearchField>
@@ -264,7 +289,7 @@ export const MenuAutocompleteListbox = (
const listBoxContent = (
<RAListBox
className={classNames.content}
className={clsx(classNames.content, styles[classNames.content])}
renderEmptyState={() => <MenuEmptyState />}
selectionMode={selectionMode}
style={{ width: newMaxWidth, maxHeight }}
@@ -273,15 +298,31 @@ export const MenuAutocompleteListbox = (
);
return (
<RAPopover className={classNames.popover} placement={placement}>
<RAPopover
className={clsx(classNames.popover, styles[classNames.popover])}
placement={placement}
>
<RAAutocomplete filter={contains}>
<RASearchField className={classNames.searchField}>
<RASearchField
className={clsx(
classNames.searchField,
styles[classNames.searchField],
)}
>
<RAInput
className={classNames.searchFieldInput}
className={clsx(
classNames.searchFieldInput,
styles[classNames.searchFieldInput],
)}
aria-label="Search"
placeholder={props.placeholder || 'Search...'}
/>
<RAButton className={classNames.searchFieldClear}>
<RAButton
className={clsx(
classNames.searchFieldClear,
styles[classNames.searchFieldClear],
)}
>
<RiCloseCircleLine />
</RAButton>
</RASearchField>
@@ -313,18 +354,30 @@ export const MenuItem = (props: MenuItemProps) => {
if (isLink && isExternal) {
return (
<RAMenuItem
className={classNames.item}
className={clsx(classNames.item, styles[classNames.item])}
data-color={color}
textValue={typeof children === 'string' ? children : undefined}
onAction={() => window.open(href, '_blank', 'noopener,noreferrer')}
{...rest}
>
<div className={classNames.itemWrapper}>
<div className={classNames.itemContent}>
<div
className={clsx(
classNames.itemWrapper,
styles[classNames.itemWrapper],
)}
>
<div
className={clsx(
classNames.itemContent,
styles[classNames.itemContent],
)}
>
{iconStart}
{children}
</div>
<div className={classNames.itemArrow}>
<div
className={clsx(classNames.itemArrow, styles[classNames.itemArrow])}
>
<RiArrowRightSLine />
</div>
</div>
@@ -334,18 +387,27 @@ export const MenuItem = (props: MenuItemProps) => {
return (
<RAMenuItem
className={classNames.item}
className={clsx(classNames.item, styles[classNames.item])}
data-color={color}
href={href}
textValue={typeof children === 'string' ? children : undefined}
{...rest}
>
<div className={classNames.itemWrapper}>
<div className={classNames.itemContent}>
<div
className={clsx(classNames.itemWrapper, styles[classNames.itemWrapper])}
>
<div
className={clsx(
classNames.itemContent,
styles[classNames.itemContent],
)}
>
{iconStart}
{children}
</div>
<div className={classNames.itemArrow}>
<div
className={clsx(classNames.itemArrow, styles[classNames.itemArrow])}
>
<RiArrowRightSLine />
</div>
</div>
@@ -363,12 +425,24 @@ export const MenuListBoxItem = (props: MenuListBoxItemProps) => {
textValue={
typeof props.children === 'string' ? props.children : undefined
}
className={classNames.itemListBox}
className={clsx(classNames.itemListBox, styles[classNames.itemListBox])}
{...rest}
>
<div className={classNames.itemWrapper}>
<div className={classNames.itemContent}>
<div className={classNames.itemListBoxCheck}>
<div
className={clsx(classNames.itemWrapper, styles[classNames.itemWrapper])}
>
<div
className={clsx(
classNames.itemContent,
styles[classNames.itemContent],
)}
>
<div
className={clsx(
classNames.itemListBoxCheck,
styles[classNames.itemListBoxCheck],
)}
>
<RiCheckLine />
</div>
{children}
@@ -383,8 +457,16 @@ export const MenuSection = (props: MenuSectionProps<object>) => {
const { classNames } = useStyles('Menu');
return (
<RAMenuSection className={classNames.section} {...props}>
<RAMenuHeader className={classNames.sectionHeader}>
<RAMenuSection
className={clsx(classNames.section, styles[classNames.section])}
{...props}
>
<RAMenuHeader
className={clsx(
classNames.sectionHeader,
styles[classNames.sectionHeader],
)}
>
{props.title}
</RAMenuHeader>
{props.children}
@@ -396,5 +478,10 @@ export const MenuSection = (props: MenuSectionProps<object>) => {
export const MenuSeparator = (props: MenuSeparatorProps) => {
const { classNames } = useStyles('Menu');
return <RAMenuSeparator className={classNames.separator} {...props} />;
return (
<RAMenuSeparator
className={clsx(classNames.separator, styles[classNames.separator])}
{...props}
/>
);
};
@@ -27,6 +27,8 @@ import { FieldError } from '../FieldError';
import type { PasswordFieldProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import { RiEyeLine, RiEyeOffLine } from '@remixicon/react';
import stylesPasswordField from './PasswordField.module.css';
import stylesTextField from '../TextField/TextField.module.css';
/** @public */
export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
@@ -76,7 +78,11 @@ export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
return (
<AriaTextField
className={clsx(passwordFieldClassNames.root, className)}
className={clsx(
passwordFieldClassNames.root,
stylesPasswordField[passwordFieldClassNames.root],
className,
)}
{...dataAttributes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
@@ -91,19 +97,30 @@ export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
description={description}
/>
<div
className={textFieldClassNames.inputWrapper}
className={clsx(
textFieldClassNames.inputWrapper,
stylesTextField[textFieldClassNames.inputWrapper],
)}
data-size={dataAttributes['data-size']}
>
{icon && (
<div
className={textFieldClassNames.inputIcon}
className={clsx(
textFieldClassNames.inputIcon,
stylesTextField[textFieldClassNames.inputIcon],
)}
data-size={dataAttributes['data-size']}
aria-hidden="true"
>
{icon}
</div>
)}
<div className={textFieldClassNames.inputAction}>
<div
className={clsx(
textFieldClassNames.inputAction,
stylesTextField[textFieldClassNames.inputAction],
)}
>
<RAButton
data-size={dataAttributes['data-size']}
data-variant={'tertiary'}
@@ -111,13 +128,19 @@ export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
aria-controls={isVisible ? 'text' : 'password'}
aria-expanded={isVisible}
onPress={() => setIsVisible(v => !v)}
className={passwordFieldClassNames.inputVisibility}
className={clsx(
passwordFieldClassNames.inputVisibility,
stylesPasswordField[passwordFieldClassNames.inputVisibility],
)}
>
{isVisible ? <RiEyeLine /> : <RiEyeOffLine />}
</RAButton>
</div>
<Input
className={textFieldClassNames.input}
className={clsx(
textFieldClassNames.input,
stylesTextField[textFieldClassNames.input],
)}
{...(icon && { 'data-icon': true })}
placeholder={placeholder}
type={isVisible ? 'text' : 'password'}
@@ -23,6 +23,7 @@ import clsx from 'clsx';
import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import { useStyles } from '../../hooks/useStyles';
import styles from './RadioGroup.module.css';
import type { RadioGroupProps, RadioProps } from './types';
@@ -57,7 +58,7 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
return (
<AriaRadioGroup
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
{...rest}
@@ -68,7 +69,9 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
secondaryLabel={secondaryLabelText}
description={description}
/>
<div className={classNames.content}>{children}</div>
<div className={clsx(classNames.content, styles[classNames.content])}>
{children}
</div>
<FieldError />
</AriaRadioGroup>
);
@@ -85,7 +88,7 @@ export const Radio = forwardRef<HTMLLabelElement, RadioProps>((props, ref) => {
return (
<AriaRadio
className={clsx(classNames.radio, className)}
className={clsx(classNames.radio, styles[classNames.radio], className)}
{...rest}
ref={ref}
/>
@@ -18,6 +18,7 @@ import { forwardRef } from 'react';
import { ScrollArea as ScrollAreaPrimitive } from '@base-ui-components/react/scroll-area';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './ScrollArea.module.css';
const ScrollAreaRoot = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
@@ -28,7 +29,7 @@ const ScrollAreaRoot = forwardRef<
return (
<ScrollAreaPrimitive.Root
ref={ref}
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
{...props}
/>
);
@@ -44,7 +45,11 @@ const ScrollAreaViewport = forwardRef<
return (
<ScrollAreaPrimitive.Viewport
ref={ref}
className={clsx(classNames.viewport, className)}
className={clsx(
classNames.viewport,
styles[classNames.viewport],
className,
)}
{...props}
/>
);
@@ -60,7 +65,11 @@ const ScrollAreaScrollbar = forwardRef<
return (
<ScrollAreaPrimitive.Scrollbar
ref={ref}
className={clsx(classNames.scrollbar, className)}
className={clsx(
classNames.scrollbar,
styles[classNames.scrollbar],
className,
)}
{...props}
/>
);
@@ -76,7 +85,7 @@ const ScrollAreaThumb = forwardRef<
return (
<ScrollAreaPrimitive.Thumb
ref={ref}
className={clsx(classNames.thumb, className)}
className={clsx(classNames.thumb, styles[classNames.thumb], className)}
{...props}
/>
);
@@ -25,6 +25,8 @@ import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import { RiSearch2Line, RiCloseCircleLine } from '@remixicon/react';
import { useStyles } from '../../hooks/useStyles';
import stylesSearchField from './SearchField.module.css';
import stylesTextField from '../TextField/TextField.module.css';
import type { SearchFieldProps } from './types';
@@ -102,6 +104,8 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
className={clsx(
textFieldClassNames.root,
searchFieldClassNames.root,
stylesTextField[textFieldClassNames.root],
stylesSearchField[searchFieldClassNames.root],
className,
)}
{...dataAttributes}
@@ -120,12 +124,18 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
description={description}
/>
<div
className={textFieldClassNames.inputWrapper}
className={clsx(
textFieldClassNames.inputWrapper,
stylesTextField[textFieldClassNames.inputWrapper],
)}
data-size={dataAttributes['data-size']}
>
{icon !== false && (
<div
className={textFieldClassNames.inputIcon}
className={clsx(
textFieldClassNames.inputIcon,
stylesTextField[textFieldClassNames.inputIcon],
)}
data-size={dataAttributes['data-size']}
aria-hidden="true"
>
@@ -133,12 +143,18 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
</div>
)}
<Input
className={textFieldClassNames.input}
className={clsx(
textFieldClassNames.input,
stylesTextField[textFieldClassNames.input],
)}
{...(icon !== false && { 'data-icon': true })}
placeholder={placeholder}
/>
<Button
className={searchFieldClassNames.clear}
className={clsx(
searchFieldClassNames.clear,
stylesSearchField[searchFieldClassNames.clear],
)}
data-size={dataAttributes['data-size']}
>
<RiCloseCircleLine />
+28 -8
View File
@@ -30,6 +30,8 @@ import { useStyles } from '../../hooks/useStyles';
import { FieldLabel } from '../FieldLabel';
import { Icon } from '../Icon';
import { FieldError } from '../FieldError';
import styles from './Select.module.css';
import stylesPopover from '../Popover/Popover.module.css';
/** @public */
export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
@@ -67,7 +69,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
return (
<AriaSelect
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
{...dataAttributes}
ref={ref}
aria-label={ariaLabel}
@@ -80,26 +82,44 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
description={description}
/>
<Button
className={classNames.trigger}
className={clsx(classNames.trigger, styles[classNames.trigger])}
data-size={dataAttributes['data-size']}
>
{icon}
<SelectValue className={classNames.value} />
<SelectValue
className={clsx(classNames.value, styles[classNames.value])}
/>
<Icon aria-hidden="true" name="chevron-down" />
</Button>
<FieldError />
<Popover className={popoverClassNames.root}>
<ListBox className={classNames.list}>
<Popover
className={clsx(
popoverClassNames.root,
stylesPopover[popoverClassNames.root],
)}
>
<ListBox className={clsx(classNames.list, styles[classNames.list])}>
{options?.map(option => (
<ListBoxItem
key={option.value}
id={option.value}
className={classNames.item}
className={clsx(classNames.item, styles[classNames.item])}
>
<div className={classNames.itemIndicator}>
<div
className={clsx(
classNames.itemIndicator,
styles[classNames.itemIndicator],
)}
>
<Icon name="check" />
</div>
<Text slot="label" className={classNames.itemLabel}>
<Text
slot="label"
className={clsx(
classNames.itemLabel,
styles[classNames.itemLabel],
)}
>
{option.label}
</Text>
</ListBoxItem>
@@ -16,6 +16,8 @@
import { useStyles } from '../../hooks/useStyles';
import { SkeletonProps } from './types';
import styles from './Skeleton.module.css';
import clsx from 'clsx';
/** @public */
export const Skeleton = (props: SkeletonProps) => {
@@ -24,7 +26,7 @@ export const Skeleton = (props: SkeletonProps) => {
return (
<div
className={classNames.root}
className={clsx(classNames.root, styles[classNames.root])}
data-rounded={rounded}
style={{
width,
+10 -2
View File
@@ -18,6 +18,8 @@ import { forwardRef } from 'react';
import { Switch as AriaSwitch } from 'react-aria-components';
import type { SwitchProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './Switch.module.css';
import clsx from 'clsx';
/** @public */
export const Switch = forwardRef<HTMLLabelElement, SwitchProps>(
@@ -25,8 +27,14 @@ export const Switch = forwardRef<HTMLLabelElement, SwitchProps>(
const { classNames } = useStyles('Switch');
return (
<AriaSwitch className={classNames.root} ref={ref} {...props}>
<div className={classNames.indicator} />
<AriaSwitch
className={clsx(classNames.root, styles[classNames.root])}
ref={ref}
{...props}
>
<div
className={clsx(classNames.indicator, styles[classNames.indicator])}
/>
{label}
</AriaSwitch>
);
@@ -20,6 +20,7 @@ import { Link } from '../../Link';
import { Cell as ReactAriaCell } from 'react-aria-components';
import type { CellProps } from '../types';
import { useStyles } from '../../../hooks/useStyles';
import styles from '../Table.module.css';
/** @public */
const Cell = (props: CellProps) => {
@@ -36,12 +37,29 @@ const Cell = (props: CellProps) => {
const { classNames } = useStyles('Table');
return (
<ReactAriaCell className={clsx(classNames.cell, className)} {...rest}>
<div className={classNames.cellContentWrapper}>
{leadingIcon && (
<div className={classNames.cellIcon}>{leadingIcon}</div>
<ReactAriaCell
className={clsx(classNames.cell, styles[classNames.cell], className)}
{...rest}
>
<div
className={clsx(
classNames.cellContentWrapper,
styles[classNames.cellContentWrapper],
)}
<div className={classNames.cellContent}>
>
{leadingIcon && (
<div
className={clsx(classNames.cellIcon, styles[classNames.cellIcon])}
>
{leadingIcon}
</div>
)}
<div
className={clsx(
classNames.cellContent,
styles[classNames.cellContent],
)}
>
{href ? (
<Link href={href} variant="body-medium" color={color}>
{title}
@@ -21,6 +21,7 @@ import { Link } from '../../Link/Link';
import { Avatar } from '@base-ui-components/react/avatar';
import { useStyles } from '../../../hooks/useStyles';
import { Cell as ReactAriaCell } from 'react-aria-components';
import styles from '../Table.module.css';
/** @public */
export const CellProfile = (props: CellProfileProps) => {
@@ -36,18 +37,39 @@ export const CellProfile = (props: CellProfileProps) => {
const { classNames } = useStyles('Table');
return (
<ReactAriaCell className={clsx(classNames.cell, className)} {...rest}>
<div className={classNames.cellContentWrapper}>
<div className={classNames.cellIcon}>
<ReactAriaCell
className={clsx(classNames.cell, styles[classNames.cell], className)}
{...rest}
>
<div
className={clsx(
classNames.cellContentWrapper,
styles[classNames.cellContentWrapper],
)}
>
<div className={clsx(classNames.cellIcon, styles[classNames.cellIcon])}>
{src && (
<Avatar.Root className={classNames.cellProfileAvatar}>
<Avatar.Root
className={clsx(
classNames.cellProfileAvatar,
styles[classNames.cellProfileAvatar],
)}
>
<Avatar.Image
src={src}
width="20"
height="20"
className={classNames.cellProfileAvatarImage}
className={clsx(
classNames.cellProfileAvatarImage,
styles[classNames.cellProfileAvatarImage],
)}
/>
<Avatar.Fallback className={classNames.cellProfileAvatarFallback}>
<Avatar.Fallback
className={clsx(
classNames.cellProfileAvatarFallback,
styles[classNames.cellProfileAvatarFallback],
)}
>
{(name || '')
.split(' ')
.map(word => word[0])
@@ -58,7 +80,12 @@ export const CellProfile = (props: CellProfileProps) => {
</Avatar.Root>
)}
</div>
<div className={classNames.cellContent}>
<div
className={clsx(
classNames.cellContent,
styles[classNames.cellContent],
)}
>
{name && href ? (
<Link href={href}>{name}</Link>
) : (
@@ -20,6 +20,8 @@ import {
} from 'react-aria-components';
import { Icon } from '../../Icon';
import { useStyles } from '../../../hooks/useStyles';
import styles from '../Table.module.css';
import clsx from 'clsx';
/** @public */
export const Column = (
@@ -28,12 +30,26 @@ export const Column = (
const { classNames } = useStyles('Table');
return (
<ReactAriaColumn className={classNames.head} {...props}>
<ReactAriaColumn
className={clsx(classNames.head, styles[classNames.head])}
{...props}
>
{({ allowsSorting, sortDirection }) => (
<div className={classNames.headContent}>
<div
className={clsx(
classNames.headContent,
styles[classNames.headContent],
)}
>
{props.children}
{allowsSorting && (
<span aria-hidden="true" className={classNames.headSortButton}>
<span
aria-hidden="true"
className={clsx(
classNames.headSortButton,
styles[classNames.headSortButton],
)}
>
{sortDirection === 'ascending' ? (
<Icon name="arrow-up" size={16} />
) : (
@@ -27,6 +27,8 @@ import { useStyles } from '../../../hooks/useStyles';
import { useNavigate } from 'react-router-dom';
import { useHref } from 'react-router-dom';
import { isExternalLink } from '../../../utils/isExternalLink';
import styles from '../Table.module.css';
import clsx from 'clsx';
/** @public */
export function Row<T extends object>({
@@ -55,7 +57,11 @@ export function Row<T extends object>({
if (isExternal) {
return (
<ReactAriaRow id={id} className={classNames.row} {...otherProps}>
<ReactAriaRow
id={id}
className={clsx(classNames.row, styles[classNames.row])}
{...otherProps}
>
{content}
</ReactAriaRow>
);
@@ -65,7 +71,7 @@ export function Row<T extends object>({
<RouterProvider navigate={navigate} useHref={useHref}>
<ReactAriaRow
id={id}
className={classNames.row}
className={clsx(classNames.row, styles[classNames.row])}
data-react-aria-pressable="true"
{...otherProps}
>
@@ -19,6 +19,8 @@ import {
Table as ReactAriaTable,
type TableProps,
} from 'react-aria-components';
import styles from '../Table.module.css';
import clsx from 'clsx';
/** @public */
export const Table = (props: TableProps) => {
@@ -26,7 +28,7 @@ export const Table = (props: TableProps) => {
return (
<ReactAriaTable
className={classNames.table}
className={clsx(classNames.table, styles[classNames.table])}
aria-label="Data table"
{...props}
/>
@@ -19,10 +19,17 @@ import {
type TableBodyProps,
} from 'react-aria-components';
import { useStyles } from '../../../hooks/useStyles';
import styles from '../Table.module.css';
import clsx from 'clsx';
/** @public */
export const TableBody = <T extends object>(props: TableBodyProps<T>) => {
const { classNames } = useStyles('Table');
return <ReactAriaTableBody className={classNames.body} {...props} />;
return (
<ReactAriaTableBody
className={clsx(classNames.body, styles[classNames.body])}
{...props}
/>
);
};
@@ -22,6 +22,8 @@ import {
import { Collection, useTableOptions } from 'react-aria-components';
import { Column } from './Column';
import { useStyles } from '../../../hooks/useStyles';
import styles from '../Table.module.css';
import clsx from 'clsx';
/** @public */
export const TableHeader = <T extends object>({
@@ -33,7 +35,9 @@ export const TableHeader = <T extends object>({
const { classNames } = useStyles('Table');
return (
<ReactAriaTableHeader className={classNames.header}>
<ReactAriaTableHeader
className={clsx(classNames.header, styles[classNames.header])}
>
{/* Add extra columns for drag and drop and selection. */}
{allowsDragging && <Column />}
{selectionBehavior === 'toggle' && (
@@ -15,27 +15,27 @@
*/
@layer components {
.bui-DataTablePagination {
.bui-TablePagination {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: var(--bui-space-5);
}
.bui-DataTablePagination--left {
.bui-TablePaginationLeft {
display: flex;
align-items: center;
justify-content: space-between;
}
.bui-DataTablePagination--right {
.bui-TablePaginationRight {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--bui-space-2);
}
.bui-DataTablePagination--select {
.bui-TablePaginationSelect {
min-width: 10.5rem;
}
}
@@ -17,6 +17,8 @@
import clsx from 'clsx';
import { Text, ButtonIcon, Select, Icon } from '../..';
import type { TablePaginationProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './TablePagination.module.css';
/**
* Pagination controls for Table components with page navigation and size selection.
@@ -38,6 +40,8 @@ export function TablePagination(props: TablePaginationProps) {
...rest
} = props;
const { classNames } = useStyles('TablePagination');
const currentOffset = offset ?? 0;
const currentPageSize = pageSize ?? 10;
@@ -65,8 +69,11 @@ export function TablePagination(props: TablePaginationProps) {
};
return (
<div className={clsx('bui-DataTablePagination', className)} {...rest}>
<div className="bui-DataTablePagination--left">
<div
className={clsx(classNames.root, styles[classNames.root], className)}
{...rest}
>
<div className={clsx(classNames.left, styles[classNames.left])}>
{showPageSizeOptions && (
<Select
name="pageSize"
@@ -86,11 +93,11 @@ export function TablePagination(props: TablePaginationProps) {
setPageSize?.(newPageSize);
onPageSizeChange?.(newPageSize);
}}
className="bui-DataTablePagination--select"
className={clsx(classNames.select, styles[classNames.select])}
/>
)}
</div>
<div className="bui-DataTablePagination--right">
<div className={clsx(classNames.right, styles[classNames.right])}>
<Text
as="p"
variant="body-medium"
+15 -6
View File
@@ -41,8 +41,9 @@ import {
RouterProvider,
TabProps as AriaTabProps,
} from 'react-aria-components';
import { useStyles } from '../../hooks/useStyles';
import styles from './Tabs.module.css';
import clsx from 'clsx';
const TabsContext = createContext<TabsContextValue | undefined>(undefined);
@@ -148,7 +149,7 @@ export const Tabs = (props: TabsProps) => {
<TabsContext.Provider value={contextValue}>
<RouterProvider navigate={navigate} useHref={useHref}>
<AriaTabs
className={classNames.tabs}
className={clsx(classNames.tabs, styles[classNames.tabs])}
keyboardActivation="manual"
selectedKey={computedSelectedKey}
ref={tabsRef}
@@ -188,9 +189,14 @@ export const TabList = (props: TabListProps) => {
});
return (
<div className={classNames.tabListWrapper}>
<div
className={clsx(
classNames.tabListWrapper,
styles[classNames.tabListWrapper],
)}
>
<AriaTabList
className={classNames.tabList}
className={clsx(classNames.tabList, styles[classNames.tabList])}
aria-label="Toolbar tabs"
{...rest}
>
@@ -219,7 +225,7 @@ export const Tab = (props: TabProps) => {
return (
<AriaTab
id={id}
className={classNames.tab}
className={clsx(classNames.tab, styles[classNames.tab])}
ref={el => setTabRef(id as string, el as HTMLDivElement)}
href={href}
{...rest}
@@ -239,7 +245,10 @@ export const TabPanel = (props: TabPanelProps) => {
const { classNames } = useStyles('Tabs');
return (
<AriaTabPanel className={classNames.panel} {...rest}>
<AriaTabPanel
className={clsx(classNames.panel, styles[classNames.panel])}
{...rest}
>
{children}
</AriaTabPanel>
);
@@ -18,6 +18,8 @@ import { TabListStateContext } from 'react-aria-components';
import { useStyles } from '../../hooks/useStyles';
import { useContext, useEffect, useCallback, useRef } from 'react';
import type { TabsIndicatorsProps } from './types';
import styles from './Tabs.module.css';
import clsx from 'clsx';
/**
* A component that renders the indicators for the toolbar.
@@ -184,8 +186,12 @@ export const TabsIndicators = (props: TabsIndicatorsProps) => {
return (
<>
<div className={classNames.tabActive} />
<div className={classNames.tabHovered} />
<div
className={clsx(classNames.tabActive, styles[classNames.tabActive])}
/>
<div
className={clsx(classNames.tabHovered, styles[classNames.tabHovered])}
/>
</>
);
};
@@ -28,6 +28,7 @@ import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { isExternalLink } from '../../utils/isExternalLink';
import { useNavigate, useHref } from 'react-router-dom';
import styles from './TagGroup.module.css';
/**
* A component that renders a list of tags.
@@ -42,9 +43,12 @@ export const TagGroup = <T extends object>({
}: TagGroupProps<T>) => {
const { classNames } = useStyles('TagGroup');
return (
<ReactAriaTagGroup className={classNames.group} {...props}>
<ReactAriaTagGroup
className={clsx(classNames.group, styles[classNames.group])}
{...props}
>
<ReactAriaTagList
className={classNames.list}
className={clsx(classNames.list, styles[classNames.list])}
items={items}
renderEmptyState={renderEmptyState}
>
@@ -70,18 +74,27 @@ export const Tag = (props: TagProps) => {
const content = (
<ReactAriaTag
textValue={textValue}
className={clsx(classNames.tag, className)}
className={clsx(classNames.tag, styles[classNames.tag], className)}
data-size={size}
href={href}
{...rest}
>
{({ allowsRemoving }) => (
<>
{icon && <span className={classNames.tagIcon}>{icon}</span>}
{icon && (
<span
className={clsx(classNames.tagIcon, styles[classNames.tagIcon])}
>
{icon}
</span>
)}
{children as ReactNode}
{allowsRemoving && (
<ReactAriaButton
className={classNames.tagRemoveButton}
className={clsx(
classNames.tagRemoveButton,
styles[classNames.tagRemoveButton],
)}
slot="remove"
>
<RiCloseCircleLine size={16} />
+2 -1
View File
@@ -19,6 +19,7 @@ import clsx from 'clsx';
import type { ElementType } from 'react';
import type { TextProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './Text.module.css';
function TextComponent<T extends ElementType = 'span'>(
{
@@ -44,7 +45,7 @@ function TextComponent<T extends ElementType = 'span'>(
return (
<Component
ref={ref}
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
data-truncate={truncate}
data-as={as}
{...dataAttributes}
@@ -19,9 +19,9 @@ import { Input, TextField as AriaTextField } from 'react-aria-components';
import clsx from 'clsx';
import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import type { TextFieldProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './TextField.module.css';
/** @public */
export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
@@ -64,7 +64,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
return (
<AriaTextField
className={clsx(classNames.root, className)}
className={clsx(classNames.root, styles[classNames.root], className)}
{...dataAttributes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
@@ -78,12 +78,18 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
description={description}
/>
<div
className={classNames.inputWrapper}
className={clsx(
classNames.inputWrapper,
styles[classNames.inputWrapper],
)}
data-size={dataAttributes['data-size']}
>
{icon && (
<div
className={classNames.inputIcon}
className={clsx(
classNames.inputIcon,
styles[classNames.inputIcon],
)}
data-size={dataAttributes['data-size']}
aria-hidden="true"
>
@@ -91,7 +97,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
</div>
)}
<Input
className={classNames.input}
className={clsx(classNames.input, styles[classNames.input])}
{...(icon && { 'data-icon': true })}
placeholder={placeholder}
/>
@@ -24,6 +24,7 @@ import {
import clsx from 'clsx';
import { TooltipProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import styles from './Tooltip.module.css';
/** @public */
export const TooltipTrigger = (props: TooltipTriggerComponentProps) => {
@@ -39,11 +40,17 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
return (
<AriaTooltip
className={clsx(classNames.tooltip, className)}
className={clsx(
classNames.tooltip,
styles[classNames.tooltip],
className,
)}
{...rest}
ref={ref}
>
<OverlayArrow className={classNames.arrow}>
<OverlayArrow
className={clsx(classNames.arrow, styles[classNames.arrow])}
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10.3356 7.39793L15.1924 3.02682C15.9269 2.36577 16.8801 2 17.8683 2H20V7.94781e-07L1.74846e-07 -9.53674e-07L0 2L1.4651 2C2.4532 2 3.4064 2.36577 4.1409 3.02682L8.9977 7.39793C9.378 7.7402 9.9553 7.74021 10.3356 7.39793Z" />
<path d="M11.0046 8.14124C10.2439 8.82575 9.08939 8.82578 8.32869 8.14122L3.47189 3.77011C2.92109 3.27432 2.20619 2.99999 1.46509 2.99999L4.10999 3L8.99769 7.39793C9.37799 7.7402 9.95529 7.7402 10.3356 7.39793L15.2226 3L17.8683 2.99999C17.1271 2.99999 16.4122 3.27432 15.8614 3.77011L11.0046 8.14124Z" />
-35
View File
@@ -23,41 +23,6 @@
@import './normalize.css';
@import './core.css';
/* Components */
@import '../components/Avatar/Avatar.styles.css';
@import '../components/Box/styles.css';
@import '../components/Button/styles.css';
@import '../components/ButtonIcon/styles.css';
@import '../components/Card/Card.styles.css';
@import '../components/Checkbox/styles.css';
@import '../components/Collapsible/Collapsible.styles.css';
@import '../components/Container/styles.css';
@import '../components/Dialog/Dialog.styles.css';
@import '../components/FieldError/FieldError.styles.css';
@import '../components/FieldLabel/FieldLabel.styles.css';
@import '../components/Flex/styles.css';
@import '../components/Grid/styles.css';
@import '../components/Header/Header.styles.css';
@import '../components/HeaderPage/HeaderPage.styles.css';
@import '../components/Icon/styles.css';
@import '../components/Link/styles.css';
@import '../components/Menu/Menu.styles.css';
@import '../components/Popover/Popover.styles.css';
@import '../components/RadioGroup/RadioGroup.styles.css';
@import '../components/Table/Table.styles.css';
@import '../components/TablePagination/TablePagination.styles.css';
@import '../components/Tabs/Tabs.styles.css';
@import '../components/TagGroup/TagGroup.styles.css';
@import '../components/Text/styles.css';
@import '../components/TextField/TextField.styles.css';
@import '../components/PasswordField/PasswordField.styles.css';
@import '../components/SearchField/SearchField.styles.css';
@import '../components/Skeleton/Skeleton.styles.css';
@import '../components/Tooltip/Tooltip.styles.css';
@import '../components/ScrollArea/ScrollArea.styles.css';
@import '../components/Select/Select.styles.css';
@import '../components/Switch/Switch.styles.css';
/* Utilities */
/* Padding */
@@ -120,6 +120,11 @@ export const componentDefinitions = {
footer: 'bui-DialogFooter',
},
},
FieldError: {
classNames: {
root: 'bui-FieldError',
},
},
FieldLabel: {
classNames: {
root: 'bui-FieldLabelWrapper',
@@ -339,6 +344,14 @@ export const componentDefinitions = {
cellProfileLink: 'bui-TableCellProfileLink',
},
},
TablePagination: {
classNames: {
root: 'bui-TablePagination',
left: 'bui-TablePaginationLeft',
right: 'bui-TablePaginationRight',
select: 'bui-TablePaginationSelect',
},
},
Tabs: {
classNames: {
tabs: 'bui-Tabs',