Add Link component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-03-13 10:38:44 +00:00
parent 75abaa6738
commit afe82bf57c
6 changed files with 264 additions and 1 deletions
@@ -0,0 +1,92 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Link } from './Link';
import { Flex } from '../Flex';
import { Text } from '../Text';
const meta = {
title: 'Components/Link',
component: Link,
args: {
children: 'Link',
},
} satisfies Meta<typeof Link>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
href: 'https://canon.backstage.io',
children: 'Sign up for Backstage',
},
};
export const AllVariants: Story = {
args: {
...Default.args,
},
render: args => (
<Flex gap="4" direction="column">
<Link href="https://canon.backstage.io" variant="subtitle" {...args} />
<Link href="https://canon.backstage.io" variant="body" {...args} />
<Link href="https://canon.backstage.io" variant="caption" {...args} />
<Link href="https://canon.backstage.io" variant="label" {...args} />
</Flex>
),
};
export const AllWeights: Story = {
args: {
...Default.args,
},
render: args => (
<Flex gap="4" direction="column">
<Link weight="regular" style={{ maxWidth: '600px' }} {...args} />
<Link weight="bold" style={{ maxWidth: '600px' }} {...args} />
</Flex>
),
};
export const Responsive: Story = {
args: {
variant: {
xs: 'label',
md: 'body',
},
},
};
export const Playground: Story = {
args: {
...Default.args,
},
render: args => (
<Flex gap="4" direction="column">
<Text>Subtitle</Text>
<Link variant="subtitle" style={{ maxWidth: '600px' }} {...args} />
<Text>Body</Text>
<Link variant="body" style={{ maxWidth: '600px' }} {...args} />
<Text>Caption</Text>
<Link variant="caption" style={{ maxWidth: '600px' }} {...args} />
<Text>Label</Text>
<Link variant="label" style={{ maxWidth: '600px' }} {...args} />
</Flex>
),
};
@@ -0,0 +1,55 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { forwardRef } from 'react';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import clsx from 'clsx';
import type { LinkProps } from './types';
/** @public */
export const Link = forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
const {
children,
variant = 'body',
weight = 'regular',
style,
className,
...restProps
} = props;
// Get the responsive values for the variant and weight
const responsiveVariant = useResponsiveValue(variant);
const responsiveWeight = useResponsiveValue(weight);
return (
<a
ref={ref}
className={clsx(
'canon-Link',
responsiveVariant && `canon-Link--variant-${responsiveVariant}`,
responsiveWeight && `canon-Link--weight-${responsiveWeight}`,
className,
)}
style={style}
{...restProps}
>
{children}
</a>
);
});
Link.displayName = 'Link';
@@ -0,0 +1,18 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Link } from './Link';
export type { LinkProps } from './types';
@@ -0,0 +1,65 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.canon-Link {
font-family: var(--canon-font-regular);
color: var(--canon-fg-link);
padding: 0;
margin: 0;
cursor: pointer;
text-decoration-line: none;
&:hover {
color: var(--canon-fg-link-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,
var(--canon-fg-link-hover) 30%,
transparent
);
}
}
.canon-Link--variant-body {
font-size: var(--canon-font-size-3);
line-height: 140%;
}
.canon-Link--variant-subtitle {
font-size: var(--canon-font-size-4);
line-height: 140%;
}
.canon-Link--variant-caption {
font-size: var(--canon-font-size-2);
line-height: 140%;
}
.canon-Link--variant-label {
font-size: var(--canon-font-size-1);
line-height: 140%;
}
.canon-Link--weight-regular {
font-weight: var(--canon-font-weight-regular);
}
.canon-Link--weight-bold {
font-weight: var(--canon-font-weight-bold);
}
@@ -0,0 +1,33 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CSSProperties, ReactNode } from 'react';
import type { Breakpoint } from '../../types';
/** @public */
export interface LinkProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
children: ReactNode;
variant?:
| 'subtitle'
| 'body'
| 'caption'
| 'label'
| Partial<Record<Breakpoint, 'subtitle' | 'body' | 'caption' | 'label'>>;
weight?: 'regular' | 'bold' | Partial<Record<Breakpoint, 'regular' | 'bold'>>;
className?: string;
style?: CSSProperties;
}
+1 -1
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
/* Components */
@import '../components/Box/styles.css';
@import '../components/Button/styles.css';
@import '../components/Flex/styles.css';
@@ -27,3 +26,4 @@
@import '../components/Heading/styles.css';
@import '../components/Input/Input.styles.css';
@import '../components/Field/Field.styles.css';
@import '../components/Link/styles.css';