Improve Link component styles
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
---
|
||||
'@backstage/ui': minor
|
||||
---
|
||||
|
||||
Added new status foreground tokens and improved Link component styling
|
||||
|
||||
**New Status Tokens:**
|
||||
|
||||
Added dedicated tokens for status colors that distinguish between usage on status backgrounds vs. standalone usage:
|
||||
|
||||
- `--bui-fg-danger-on-bg` / `--bui-fg-danger`
|
||||
- `--bui-fg-warning-on-bg` / `--bui-fg-warning`
|
||||
- `--bui-fg-success-on-bg` / `--bui-fg-success`
|
||||
- `--bui-fg-info-on-bg` / `--bui-fg-info`
|
||||
|
||||
The `-on-bg` variants are designed for text on colored backgrounds, while the base variants are for standalone status indicators with improved visibility and contrast.
|
||||
|
||||
**Migration:**
|
||||
|
||||
If you're using status foreground colors on colored backgrounds, update to the new `-on-bg` tokens:
|
||||
|
||||
```diff
|
||||
.error-badge {
|
||||
- color: var(--bui-fg-danger);
|
||||
+ color: var(--bui-fg-danger-on-bg);
|
||||
background: var(--bui-bg-danger);
|
||||
}
|
||||
```
|
||||
|
||||
For standalone status indicators (icons, badges, text), continue using the base tokens which now have updated values for better visibility.
|
||||
|
||||
**Link Component Updates:**
|
||||
|
||||
1. **New `standalone` prop**: Links now have a `standalone` variant that removes the default underline (shows only on hover)
|
||||
2. **New `info` color**: Added support for `color="info"`
|
||||
3. **Improved default underline styling**: Links now show underlines by default with refined styling using `color-mix` for better visual hierarchy
|
||||
|
||||
```tsx
|
||||
// Default link - shows underline by default
|
||||
<Link href="/">Sign up</Link>
|
||||
|
||||
// Standalone link - underline only on hover
|
||||
<Link href="/" standalone>Sign up</Link>
|
||||
|
||||
// Info color link
|
||||
<Link href="/" color="info">Learn more</Link>
|
||||
```
|
||||
|
||||
**Affected components:** Link
|
||||
@@ -7,7 +7,9 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
export const Default = () => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Link href="/">Sign up for Backstage</Link>
|
||||
<Link href="/" variant="body-large">
|
||||
Sign up for Backstage
|
||||
</Link>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +17,7 @@ export const Default = () => {
|
||||
export const ExternalLink = () => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Link href="https://backstage.io" target="_blank">
|
||||
<Link href="https://backstage.io" target="_blank" variant="body-large">
|
||||
Sign up for Backstage
|
||||
</Link>
|
||||
</MemoryRouter>
|
||||
@@ -58,22 +60,25 @@ export const AllVariants = () => {
|
||||
export const AllColors = () => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Flex gap="4" direction="column">
|
||||
<Link href="#" color="primary">
|
||||
<Flex gap="2" direction="column">
|
||||
<Link href="#" color="primary" variant="body-large">
|
||||
Primary
|
||||
</Link>
|
||||
<Link href="#" color="secondary">
|
||||
<Link href="#" color="secondary" variant="body-large">
|
||||
Secondary
|
||||
</Link>
|
||||
<Link href="#" color="danger">
|
||||
<Link href="#" color="danger" variant="body-large">
|
||||
Danger
|
||||
</Link>
|
||||
<Link href="#" color="warning">
|
||||
<Link href="#" color="warning" variant="body-large">
|
||||
Warning
|
||||
</Link>
|
||||
<Link href="#" color="success">
|
||||
<Link href="#" color="success" variant="body-large">
|
||||
Success
|
||||
</Link>
|
||||
<Link href="#" color="info" variant="body-large">
|
||||
Info
|
||||
</Link>
|
||||
</Flex>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -83,13 +88,28 @@ export const Weight = () => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Flex gap="4">
|
||||
<Link href="#" weight="regular">
|
||||
<Link href="#" weight="regular" variant="body-large">
|
||||
Regular
|
||||
</Link>
|
||||
<Link href="#" weight="bold">
|
||||
<Link href="#" weight="bold" variant="body-large">
|
||||
Bold
|
||||
</Link>
|
||||
</Flex>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export const Standalone = () => {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<Flex gap="4">
|
||||
<Link href="#" variant="body-large">
|
||||
Default link
|
||||
</Link>
|
||||
<Link href="#" variant="body-large" standalone>
|
||||
Standalone link
|
||||
</Link>
|
||||
</Flex>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
allVariantsSnippet,
|
||||
allColorsSnippet,
|
||||
weightSnippet,
|
||||
standaloneSnippet,
|
||||
} from './snippets';
|
||||
import {
|
||||
Default,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
AllVariants,
|
||||
AllColors,
|
||||
Weight,
|
||||
Standalone,
|
||||
} from './components';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
@@ -55,6 +57,7 @@ Use `target="_blank"` to open links in a new tab.
|
||||
py={4}
|
||||
preview={<ExternalLink />}
|
||||
code={externalLinkSnippet}
|
||||
layout="side-by-side"
|
||||
/>
|
||||
|
||||
### Variants
|
||||
@@ -79,7 +82,13 @@ Status colors for contextual links.
|
||||
|
||||
### Weight
|
||||
|
||||
<Snippet align="center" py={4} preview={<Weight />} code={weightSnippet} />
|
||||
<Snippet align="center" py={4} preview={<Weight />} code={weightSnippet} layout="side-by-side" />
|
||||
|
||||
### Standalone
|
||||
|
||||
Use `standalone` to remove the underline by default. The underline will appear on hover.
|
||||
|
||||
<Snippet align="center" py={4} preview={<Standalone />} code={standaloneSnippet} layout="side-by-side" />
|
||||
|
||||
<Theming definition={LinkDefinition} />
|
||||
|
||||
|
||||
@@ -66,6 +66,12 @@ export const linkPropDefs: Record<string, PropDef> = {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Truncates text with ellipsis when it overflows its container.',
|
||||
default: 'false',
|
||||
},
|
||||
standalone: {
|
||||
type: 'boolean',
|
||||
description: 'Removes underline by default. Underline appears on hover.',
|
||||
default: 'false',
|
||||
},
|
||||
...childrenPropDefs,
|
||||
...classNamePropDefs,
|
||||
|
||||
@@ -4,7 +4,7 @@ export const linkUsageSnippet = `import { Link } from '@backstage/ui';
|
||||
|
||||
export const defaultSnippet = `<Link href="/">Sign up for Backstage</Link>`;
|
||||
|
||||
export const externalLinkSnippet = `<Link href="https://backstage.io" target="_blank">
|
||||
export const externalLinkSnippet = `<Link href="#" target="_blank">
|
||||
Sign up for Backstage
|
||||
</Link>`;
|
||||
|
||||
@@ -25,9 +25,15 @@ export const allColorsSnippet = `<Flex gap="4" direction="column">
|
||||
<Link href="#" color="danger">Danger</Link>
|
||||
<Link href="#" color="warning">Warning</Link>
|
||||
<Link href="#" color="success">Success</Link>
|
||||
<Link href="#" color="info">Info</Link>
|
||||
</Flex>`;
|
||||
|
||||
export const weightSnippet = `<Flex gap="4">
|
||||
<Link href="#" weight="regular">Regular</Link>
|
||||
<Link href="#" weight="bold">Bold</Link>
|
||||
</Flex>`;
|
||||
|
||||
export const standaloneSnippet = `<Flex gap="4">
|
||||
<Link href="#">Default link</Link>
|
||||
<Link href="#" standalone>Standalone link</Link>
|
||||
</Flex>`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -88,10 +88,14 @@
|
||||
--bui-fg-solid-disabled: #98a8bc;
|
||||
--bui-fg-tint: #1f5493;
|
||||
--bui-fg-tint-disabled: var(--bui-gray-5);
|
||||
--bui-fg-danger: #991919;
|
||||
--bui-fg-warning: #92310a;
|
||||
--bui-fg-success: #116932;
|
||||
--bui-fg-info: #173da6;
|
||||
--bui-fg-danger-on-bg: #991919;
|
||||
--bui-fg-warning-on-bg: #92310a;
|
||||
--bui-fg-success-on-bg: #116932;
|
||||
--bui-fg-info-on-bg: #173da6;
|
||||
--bui-fg-danger: #ec3b18;
|
||||
--bui-fg-warning: #ef7a32;
|
||||
--bui-fg-success: #1ed760;
|
||||
--bui-fg-info: #0d74ce;
|
||||
--bui-border: #0000001a;
|
||||
--bui-border-hover: #0003;
|
||||
--bui-border-pressed: #0006;
|
||||
@@ -153,10 +157,14 @@
|
||||
--bui-fg-solid-disabled: #6191cc;
|
||||
--bui-fg-tint: #9cc9ff;
|
||||
--bui-fg-tint-disabled: var(--bui-gray-5);
|
||||
--bui-fg-danger: #fca5a5;
|
||||
--bui-fg-warning: #fdba74;
|
||||
--bui-fg-success: #86efac;
|
||||
--bui-fg-info: #a3cfff;
|
||||
--bui-fg-danger-on-bg: #fca5a5;
|
||||
--bui-fg-warning-on-bg: #fdba74;
|
||||
--bui-fg-success-on-bg: #86efac;
|
||||
--bui-fg-info-on-bg: #a3cfff;
|
||||
--bui-fg-danger: #ff5a30;
|
||||
--bui-fg-warning: #ffa057;
|
||||
--bui-fg-success: #1ed760;
|
||||
--bui-fg-info: #70b8ff;
|
||||
--bui-border: #ffffff1f;
|
||||
--bui-border-hover: #fff6;
|
||||
--bui-border-pressed: #ffffff80;
|
||||
|
||||
@@ -1115,6 +1115,7 @@ export const LinkDefinition: {
|
||||
'success',
|
||||
];
|
||||
readonly truncate: readonly [true, false];
|
||||
readonly standalone: readonly [true, false];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1128,6 +1129,8 @@ export interface LinkProps extends LinkProps_2 {
|
||||
| TextColorStatus
|
||||
| Partial<Record<Breakpoint, TextColors | TextColorStatus>>;
|
||||
// (undocumented)
|
||||
standalone?: boolean;
|
||||
// (undocumented)
|
||||
title?: string;
|
||||
// (undocumented)
|
||||
truncate?: boolean;
|
||||
@@ -1906,7 +1909,7 @@ export { Text_2 as Text };
|
||||
export type TextColors = 'primary' | 'secondary';
|
||||
|
||||
// @public (undocumented)
|
||||
export type TextColorStatus = 'danger' | 'warning' | 'success';
|
||||
export type TextColorStatus = 'danger' | 'warning' | 'success' | 'info';
|
||||
|
||||
// @public
|
||||
export const TextDefinition: {
|
||||
|
||||
@@ -36,22 +36,22 @@
|
||||
|
||||
.bui-Alert[data-status='info'] {
|
||||
--alert-bg: var(--bui-bg-info);
|
||||
--alert-fg: var(--bui-fg-info);
|
||||
--alert-fg: var(--bui-fg-info-on-bg);
|
||||
}
|
||||
|
||||
.bui-Alert[data-status='success'] {
|
||||
--alert-bg: var(--bui-bg-success);
|
||||
--alert-fg: var(--bui-fg-success);
|
||||
--alert-fg: var(--bui-fg-success-on-bg);
|
||||
}
|
||||
|
||||
.bui-Alert[data-status='warning'] {
|
||||
--alert-bg: var(--bui-bg-warning);
|
||||
--alert-fg: var(--bui-fg-warning);
|
||||
--alert-fg: var(--bui-fg-warning-on-bg);
|
||||
}
|
||||
|
||||
.bui-Alert[data-status='danger'] {
|
||||
--alert-bg: var(--bui-bg-danger);
|
||||
--alert-fg: var(--bui-fg-danger);
|
||||
--alert-fg: var(--bui-fg-danger-on-bg);
|
||||
}
|
||||
|
||||
.bui-AlertIcon {
|
||||
|
||||
@@ -22,9 +22,14 @@
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
text-decoration-line: none;
|
||||
display: inline-block;
|
||||
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-thickness: min(2px, max(1px, 0.05em));
|
||||
text-underline-offset: calc(0.025em + 2px);
|
||||
text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
|
||||
|
||||
&:hover {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: solid;
|
||||
@@ -102,9 +107,25 @@
|
||||
color: var(--bui-fg-success);
|
||||
}
|
||||
|
||||
.bui-Link[data-color='info'] {
|
||||
color: var(--bui-fg-info);
|
||||
}
|
||||
|
||||
.bui-Link[data-truncate] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bui-Link[data-standalone] {
|
||||
text-decoration-line: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-thickness: min(2px, max(1px, 0.05em));
|
||||
text-underline-offset: calc(0.025em + 2px);
|
||||
text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,12 @@ export const AllColors = meta.story({
|
||||
color="success"
|
||||
children="I am success"
|
||||
/>
|
||||
<Link
|
||||
href="https://ui.backstage.io"
|
||||
variant="title-small"
|
||||
color="info"
|
||||
children="I am info"
|
||||
/>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
@@ -235,6 +241,25 @@ export const Truncate = meta.story({
|
||||
},
|
||||
});
|
||||
|
||||
export const Standalone = meta.story({
|
||||
args: {
|
||||
href: '/',
|
||||
children: 'Standalone link (no underline by default)',
|
||||
standalone: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const StandaloneComparison = meta.story({
|
||||
render: () => (
|
||||
<Flex gap="4" direction="column">
|
||||
<Text>Default link (underline by default):</Text>
|
||||
<Link href="/" children="Sign up for Backstage" />
|
||||
<Text>Standalone link (underline on hover only):</Text>
|
||||
<Link href="/" standalone children="Sign up for Backstage" />
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
|
||||
export const Responsive = meta.story({
|
||||
args: {
|
||||
...Default.input.args,
|
||||
@@ -244,27 +269,3 @@ export const Responsive = meta.story({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Playground = meta.story({
|
||||
args: {
|
||||
...Default.input.args,
|
||||
},
|
||||
render: args => (
|
||||
<Flex gap="4" direction="column">
|
||||
<Text>Title X Small</Text>
|
||||
<Link variant="title-x-small" style={{ maxWidth: '600px' }} {...args} />
|
||||
<Text>Body X Small</Text>
|
||||
<Link variant="body-x-small" style={{ maxWidth: '600px' }} {...args} />
|
||||
<Text>Body Small</Text>
|
||||
<Link variant="body-small" style={{ maxWidth: '600px' }} {...args} />
|
||||
<Text>Body Medium</Text>
|
||||
<Link variant="body-medium" style={{ maxWidth: '600px' }} {...args} />
|
||||
<Text>Body Large</Text>
|
||||
<Link variant="body-large" style={{ maxWidth: '600px' }} {...args} />
|
||||
<Text>Title Small</Text>
|
||||
<Link variant="title-small" style={{ maxWidth: '600px' }} {...args} />
|
||||
<Text>Title Medium</Text>
|
||||
<Link variant="title-medium" style={{ maxWidth: '600px' }} {...args} />
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ const LinkInternal = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
|
||||
weight,
|
||||
color,
|
||||
truncate,
|
||||
standalone,
|
||||
slot,
|
||||
...restProps
|
||||
} = cleanedProps;
|
||||
|
||||
@@ -29,5 +29,6 @@ export const LinkDefinition = {
|
||||
weight: ['regular', 'bold'] as const,
|
||||
color: ['primary', 'secondary', 'danger', 'warning', 'success'] as const,
|
||||
truncate: [true, false] as const,
|
||||
standalone: [true, false] as const,
|
||||
},
|
||||
} as const satisfies ComponentDefinition;
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface LinkProps extends AriaLinkProps {
|
||||
| TextColorStatus
|
||||
| Partial<Record<Breakpoint, TextColors | TextColorStatus>>;
|
||||
truncate?: boolean;
|
||||
standalone?: boolean;
|
||||
|
||||
// This is used to set the title attribute on the link
|
||||
title?: string;
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
--bui-bg-warning: #ffedd5;
|
||||
--bui-bg-success: #dcfce7;
|
||||
--bui-bg-info: #dbeafe;
|
||||
|
||||
|
||||
/* Foreground colors */
|
||||
--bui-fg-primary: var(--bui-black);
|
||||
@@ -129,10 +130,16 @@
|
||||
--bui-fg-solid-disabled: #98a8bc;
|
||||
--bui-fg-tint: #1f5493;
|
||||
--bui-fg-tint-disabled: var(--bui-gray-5);
|
||||
--bui-fg-danger: #991919;
|
||||
--bui-fg-warning: #92310a;
|
||||
--bui-fg-success: #116932;
|
||||
--bui-fg-info: #173da6;
|
||||
|
||||
/* Foreground Statuses */
|
||||
--bui-fg-danger-on-bg: #991919;
|
||||
--bui-fg-warning-on-bg: #92310a;
|
||||
--bui-fg-success-on-bg: #116932;
|
||||
--bui-fg-info-on-bg: #173da6;
|
||||
--bui-fg-danger: #EC3B18;
|
||||
--bui-fg-warning: #EF7A32;
|
||||
--bui-fg-success: #1ED760;
|
||||
--bui-fg-info: #0D74CE;
|
||||
|
||||
/* Border colors */
|
||||
--bui-border: rgba(0, 0, 0, 0.1);
|
||||
@@ -217,10 +224,16 @@
|
||||
--bui-fg-solid-disabled: #6191cc;
|
||||
--bui-fg-tint: #9cc9ff;
|
||||
--bui-fg-tint-disabled: var(--bui-gray-5);
|
||||
--bui-fg-danger: #fca5a5;
|
||||
--bui-fg-warning: #fdba74;
|
||||
--bui-fg-success: #86efac;
|
||||
--bui-fg-info: #a3cfff;
|
||||
|
||||
/* Foreground statuses */
|
||||
--bui-fg-danger-on-bg: #fca5a5;
|
||||
--bui-fg-warning-on-bg: #fdba74;
|
||||
--bui-fg-success-on-bg: #86efac;
|
||||
--bui-fg-info-on-bg: #a3cfff;
|
||||
--bui-fg-danger: #FF5A30;
|
||||
--bui-fg-warning: #FFA057;
|
||||
--bui-fg-success: #1ED760;
|
||||
--bui-fg-info: #70B8FF;
|
||||
|
||||
/* Border colors */
|
||||
--bui-border: rgba(255, 255, 255, 0.12);
|
||||
|
||||
@@ -131,7 +131,7 @@ export type TextVariants =
|
||||
export type TextColors = 'primary' | 'secondary';
|
||||
|
||||
/** @public */
|
||||
export type TextColorStatus = 'danger' | 'warning' | 'success';
|
||||
export type TextColorStatus = 'danger' | 'warning' | 'success' | 'info';
|
||||
|
||||
/** @public */
|
||||
export type TextWeights = 'regular' | 'bold';
|
||||
|
||||
Reference in New Issue
Block a user