feat(ui): add danger variant to Button component
Added a `danger` variant for destructive actions like delete or remove. Uses solid red background with white text in both light and dark modes. - Added CSS styles with custom properties for future token migration - Updated Storybook stories to include danger variant examples - Updated docs-ui documentation to reflect the new variant Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Added `destructive` prop to Button for dangerous actions like delete or remove. Works with all variants (primary, secondary, tertiary).
|
||||
|
||||
**Affected components:** Button
|
||||
@@ -59,6 +59,22 @@ export const Disabled = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Destructive = () => {
|
||||
return (
|
||||
<Flex gap="4">
|
||||
<Button variant="primary" destructive>
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="secondary" destructive>
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="tertiary" destructive>
|
||||
Tertiary
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<Button variant="primary" loading={true}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
sizesSnippet,
|
||||
withIconsSnippet,
|
||||
disabledSnippet,
|
||||
destructiveSnippet,
|
||||
loadingSnippet,
|
||||
asLinkSnippet,
|
||||
buttonSnippetUsage,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
Sizes,
|
||||
WithIcons,
|
||||
Disabled,
|
||||
Destructive,
|
||||
Loading,
|
||||
AsLink,
|
||||
} from './components';
|
||||
@@ -32,7 +34,7 @@ export const reactAriaUrls = {
|
||||
|
||||
<PageTitle
|
||||
title="Button"
|
||||
description="A button with primary, secondary, and tertiary variants and loading state."
|
||||
description="A button with primary, secondary, and tertiary variants, destructive styling, and loading state."
|
||||
/>
|
||||
|
||||
<Snippet align="center" py={4} preview={<Variants />} code={variantsSnippet} />
|
||||
@@ -95,6 +97,19 @@ Icons can appear before or after the label.
|
||||
layout="side-by-side"
|
||||
/>
|
||||
|
||||
### Destructive
|
||||
|
||||
Use the `destructive` prop for dangerous actions like delete or remove.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<Destructive />}
|
||||
code={destructiveSnippet}
|
||||
layout="side-by-side"
|
||||
/>
|
||||
|
||||
### Loading
|
||||
|
||||
Shows a spinner and disables interaction during async operations.
|
||||
|
||||
@@ -16,6 +16,12 @@ export const buttonPropDefs: Record<string, PropDef> = {
|
||||
</>
|
||||
),
|
||||
},
|
||||
destructive: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description:
|
||||
'Applies destructive styling for dangerous actions like delete or remove.',
|
||||
},
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium'],
|
||||
|
||||
@@ -43,6 +43,18 @@ export const disabledSnippet = `<Flex gap="4">
|
||||
</Button>
|
||||
</Flex>`;
|
||||
|
||||
export const destructiveSnippet = `<Flex gap="4">
|
||||
<Button variant="primary" destructive>
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="secondary" destructive>
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="tertiary" destructive>
|
||||
Tertiary
|
||||
</Button>
|
||||
</Flex>`;
|
||||
|
||||
export const loadingSnippet = `<Button variant="primary" loading={true}>
|
||||
Load more items
|
||||
</Button>`;
|
||||
|
||||
@@ -330,6 +330,9 @@ export const ButtonDefinition: {
|
||||
readonly dataAttribute: true;
|
||||
readonly default: 'primary';
|
||||
};
|
||||
readonly destructive: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly loading: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
@@ -445,6 +448,7 @@ export interface ButtonLinkProps
|
||||
export type ButtonOwnProps = LeafSurfaceProps & {
|
||||
size?: Responsive<'small' | 'medium'>;
|
||||
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
|
||||
destructive?: boolean;
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
loading?: boolean;
|
||||
|
||||
@@ -79,6 +79,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='primary'][data-destructive='true'] {
|
||||
/* Custom properties matching future token names (without bui- prefix) */
|
||||
--bg-solid-danger: #dc2626;
|
||||
--bg-solid-danger-hover: #b91c1c;
|
||||
--bg-solid-danger-pressed: #991b1b;
|
||||
--bg-solid-danger-disabled: #fca5a5;
|
||||
--fg-solid-danger: var(--bui-white);
|
||||
|
||||
[data-theme-mode='dark'] & {
|
||||
--bg-solid-danger: #ef4444;
|
||||
--bg-solid-danger-hover: #dc2626;
|
||||
--bg-solid-danger-pressed: #b91c1c;
|
||||
--bg-solid-danger-disabled: #7f1d1d;
|
||||
}
|
||||
|
||||
--bg: var(--bg-solid-danger);
|
||||
--bg-hover: var(--bg-solid-danger-hover);
|
||||
--bg-active: var(--bg-solid-danger-pressed);
|
||||
--fg: var(--fg-solid-danger);
|
||||
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
--bg: var(--bg-solid-danger-disabled);
|
||||
--bg-hover: var(--bg-solid-danger-disabled);
|
||||
--bg-active: var(--bg-solid-danger-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bui-border-danger);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='secondary'] {
|
||||
--bg: var(--bui-bg-neutral-on-surface-0);
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-0-hover);
|
||||
@@ -117,6 +150,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='secondary'][data-destructive='true'] {
|
||||
/* Custom properties for hover/active states (no tokens exist yet) */
|
||||
--bg-danger-hover: #fecaca;
|
||||
--bg-danger-pressed: #fca5a5;
|
||||
|
||||
[data-theme-mode='dark'] & {
|
||||
--bg-danger-hover: #450a0a;
|
||||
--bg-danger-pressed: #7f1d1d;
|
||||
}
|
||||
|
||||
--bg: var(--bui-bg-danger);
|
||||
--bg-hover: var(--bg-danger-hover);
|
||||
--bg-active: var(--bg-danger-pressed);
|
||||
--fg: var(--bui-fg-danger);
|
||||
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
--bg-hover: var(--bg);
|
||||
--bg-active: var(--bg);
|
||||
--fg: var(--bui-fg-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 0 0 0 2px var(--bui-border-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='tertiary'] {
|
||||
--bg-hover: var(--bui-bg-neutral-on-surface-0-hover);
|
||||
--bg-active: var(--bui-bg-neutral-on-surface-0-pressed);
|
||||
@@ -151,6 +211,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-variant='tertiary'][data-destructive='true'] {
|
||||
/* Custom properties for hover/active states (no tokens exist yet) */
|
||||
--bg-danger-hover: #fecaca;
|
||||
--bg-danger-pressed: #fca5a5;
|
||||
|
||||
[data-theme-mode='dark'] & {
|
||||
--bg-danger-hover: #450a0a;
|
||||
--bg-danger-pressed: #7f1d1d;
|
||||
}
|
||||
|
||||
--bg-hover: var(--bui-bg-danger);
|
||||
--bg-active: var(--bg-danger-pressed);
|
||||
--fg: var(--bui-fg-danger);
|
||||
|
||||
&[data-disabled='true'],
|
||||
&[data-loading='true'] {
|
||||
--bg-hover: var(--bg);
|
||||
--bg-active: var(--bg);
|
||||
--fg: var(--bui-fg-disabled);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 0 0 0 2px var(--bui-border-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.bui-Button[data-size='small'] {
|
||||
font-size: var(--bui-font-size-3);
|
||||
padding: 0 var(--bui-space-2);
|
||||
|
||||
@@ -31,7 +31,10 @@ const meta = preview.meta({
|
||||
},
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary'],
|
||||
options: ['primary', 'secondary', 'tertiary'],
|
||||
},
|
||||
destructive: {
|
||||
control: 'boolean',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -67,6 +70,15 @@ export const Variants = meta.story({
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
@@ -81,6 +93,15 @@ export const Variants = meta.story({
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
@@ -95,6 +116,15 @@ export const Variants = meta.story({
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
@@ -109,6 +139,15 @@ export const Variants = meta.story({
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
@@ -123,6 +162,98 @@ export const Variants = meta.story({
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary">
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="primary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="secondary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
<Button iconStart={<RiCloudLine />} variant="tertiary" destructive>
|
||||
Button
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
|
||||
export const Destructive = meta.story({
|
||||
render: () => (
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Primary Destructive</Text>
|
||||
<Flex align="center" p="4" gap="4">
|
||||
<Button variant="primary" destructive>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="primary" destructive iconStart={<RiCloudLine />}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="primary" destructive isDisabled>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button variant="primary" destructive loading>
|
||||
Loading
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Secondary Destructive</Text>
|
||||
<Flex align="center" p="4" gap="4">
|
||||
<Button variant="secondary" destructive>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="secondary" destructive iconStart={<RiCloudLine />}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="secondary" destructive isDisabled>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button variant="secondary" destructive loading>
|
||||
Loading
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Tertiary Destructive</Text>
|
||||
<Flex align="center" p="4" gap="4">
|
||||
<Button variant="tertiary" destructive>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="tertiary" destructive iconStart={<RiCloudLine />}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="tertiary" destructive isDisabled>
|
||||
Disabled
|
||||
</Button>
|
||||
<Button variant="tertiary" destructive loading>
|
||||
Loading
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>On Surface 1</Text>
|
||||
<Flex align="center" surface="1" p="4" gap="4">
|
||||
<Button variant="primary" destructive>
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="secondary" destructive>
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="tertiary" destructive>
|
||||
Tertiary
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="4">
|
||||
<Text>Sizes</Text>
|
||||
<Flex align="center" p="4" gap="4">
|
||||
<Button variant="primary" destructive size="small">
|
||||
Small
|
||||
</Button>
|
||||
<Button variant="primary" destructive size="medium">
|
||||
Medium
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -181,16 +312,29 @@ export const FullWidth = meta.story({
|
||||
|
||||
export const Disabled = meta.story({
|
||||
render: () => (
|
||||
<Flex direction="row" gap="4">
|
||||
<Button variant="primary" isDisabled>
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="secondary" isDisabled>
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="tertiary" isDisabled>
|
||||
Tertiary
|
||||
</Button>
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex direction="row" gap="4">
|
||||
<Button variant="primary" isDisabled>
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="secondary" isDisabled>
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="tertiary" isDisabled>
|
||||
Tertiary
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex direction="row" gap="4">
|
||||
<Button variant="primary" destructive isDisabled>
|
||||
Primary Destructive
|
||||
</Button>
|
||||
<Button variant="secondary" destructive isDisabled>
|
||||
Secondary Destructive
|
||||
</Button>
|
||||
<Button variant="tertiary" destructive isDisabled>
|
||||
Tertiary Destructive
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
@@ -270,6 +414,24 @@ export const LoadingVariants = meta.story({
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Text>Primary Destructive</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<Button variant="primary" destructive size="small" loading>
|
||||
Small Loading
|
||||
</Button>
|
||||
<Button variant="primary" destructive size="medium" loading>
|
||||
Medium Loading
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
destructive
|
||||
loading
|
||||
iconStart={<RiCloudLine />}
|
||||
>
|
||||
With Icon
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Text>Loading vs Disabled</Text>
|
||||
<Flex align="center" gap="4">
|
||||
<Button variant="primary" loading>
|
||||
|
||||
@@ -33,6 +33,7 @@ export const ButtonDefinition = defineComponent<ButtonOwnProps>()({
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
variant: { dataAttribute: true, default: 'primary' },
|
||||
destructive: { dataAttribute: true },
|
||||
loading: { dataAttribute: true },
|
||||
iconStart: {},
|
||||
iconEnd: {},
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { LeafSurfaceProps, Responsive } from '../../types';
|
||||
export type ButtonOwnProps = LeafSurfaceProps & {
|
||||
size?: Responsive<'small' | 'medium'>;
|
||||
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
|
||||
destructive?: boolean;
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
loading?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user