Add new Tabs component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-05-18 20:53:59 +01:00
parent 33eacde752
commit 9510105a49
11 changed files with 478 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': patch
---
Add new Tabs component to Canon
+77
View File
@@ -349,6 +349,83 @@
display: flex;
}
.canon-TabsRoot {
border: 1px solid var(--color-gray-200);
border-radius: .375rem;
}
.canon-TabsList {
z-index: 0;
display: flex;
position: relative;
}
.canon-TabsTab {
appearance: none;
color: var(--canon-fg-secondary);
user-select: none;
padding-inline: var(--canon-space-3);
height: 2rem;
font-family: inherit;
font-size: .875rem;
font-weight: 500;
line-height: 1.25rem;
font-size: var(--canon-font-size-2);
cursor: pointer;
background: none;
border: 0;
outline: 0;
justify-content: center;
align-items: center;
margin: 0;
padding-block: 0;
transition: color .2s ease-in-out;
display: flex;
&[data-selected] {
color: var(--canon-fg-primary);
}
@media (hover: hover) {
&:hover {
color: var(--canon-fg-primary);
}
}
&:focus-visible {
position: relative;
&:before {
content: "";
outline: 1px solid var(--canon-ring);
outline-offset: -1px;
border-radius: .25rem;
position: absolute;
inset: .25rem 0;
}
}
}
.canon-TabsIndicator {
z-index: -1;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
background-color: var(--canon-bg-solid);
height: 1px;
transition-property: translate, width;
transition-duration: .2s;
transition-timing-function: ease-in-out;
position: absolute;
bottom: 0;
left: 0;
}
.canon-TabsPanel {
&[hidden] {
display: none;
}
}
.canon-Text {
font-family: var(--canon-font-regular);
margin: 0;
+77
View File
@@ -9573,6 +9573,83 @@
display: flex;
}
.canon-TabsRoot {
border: 1px solid var(--color-gray-200);
border-radius: .375rem;
}
.canon-TabsList {
z-index: 0;
display: flex;
position: relative;
}
.canon-TabsTab {
appearance: none;
color: var(--canon-fg-secondary);
user-select: none;
padding-inline: var(--canon-space-3);
height: 2rem;
font-family: inherit;
font-size: .875rem;
font-weight: 500;
line-height: 1.25rem;
font-size: var(--canon-font-size-2);
cursor: pointer;
background: none;
border: 0;
outline: 0;
justify-content: center;
align-items: center;
margin: 0;
padding-block: 0;
transition: color .2s ease-in-out;
display: flex;
&[data-selected] {
color: var(--canon-fg-primary);
}
@media (hover: hover) {
&:hover {
color: var(--canon-fg-primary);
}
}
&:focus-visible {
position: relative;
&:before {
content: "";
outline: 1px solid var(--canon-ring);
outline-offset: -1px;
border-radius: .25rem;
position: absolute;
inset: .25rem 0;
}
}
}
.canon-TabsIndicator {
z-index: -1;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
background-color: var(--canon-bg-solid);
height: 1px;
transition-property: translate, width;
transition-duration: .2s;
transition-timing-function: ease-in-out;
position: absolute;
bottom: 0;
left: 0;
}
.canon-TabsPanel {
&[hidden] {
display: none;
}
}
.canon-Text {
font-family: var(--canon-font-regular);
margin: 0;
+76
View File
@@ -0,0 +1,76 @@
.canon-TabsRoot {
border: 1px solid var(--color-gray-200);
border-radius: .375rem;
}
.canon-TabsList {
z-index: 0;
display: flex;
position: relative;
}
.canon-TabsTab {
appearance: none;
color: var(--canon-fg-secondary);
user-select: none;
padding-inline: var(--canon-space-3);
height: 2rem;
font-family: inherit;
font-size: .875rem;
font-weight: 500;
line-height: 1.25rem;
font-size: var(--canon-font-size-2);
cursor: pointer;
background: none;
border: 0;
outline: 0;
justify-content: center;
align-items: center;
margin: 0;
padding-block: 0;
transition: color .2s ease-in-out;
display: flex;
&[data-selected] {
color: var(--canon-fg-primary);
}
@media (hover: hover) {
&:hover {
color: var(--canon-fg-primary);
}
}
&:focus-visible {
position: relative;
&:before {
content: "";
outline: 1px solid var(--canon-ring);
outline-offset: -1px;
border-radius: .25rem;
position: absolute;
inset: .25rem 0;
}
}
}
.canon-TabsIndicator {
z-index: -1;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
background-color: var(--canon-bg-solid);
height: 1px;
transition-property: translate, width;
transition-duration: .2s;
transition-timing-function: ease-in-out;
position: absolute;
bottom: 0;
left: 0;
}
.canon-TabsPanel {
&[hidden] {
display: none;
}
}
+21
View File
@@ -23,6 +23,7 @@ import { RefAttributes } from 'react';
import type { RemixiconComponentType } from '@remixicon/react';
import { ScrollArea as ScrollArea_2 } from '@base-ui-components/react/scroll-area';
import { Table as Table_2 } from '@tanstack/react-table';
import { Tabs as Tabs_2 } from '@base-ui-components/react/tabs';
import { TdHTMLAttributes } from 'react';
import { ThHTMLAttributes } from 'react';
import { Tooltip as Tooltip_2 } from '@base-ui-components/react/tooltip';
@@ -1231,6 +1232,26 @@ export interface TableCellTextProps
title: string;
}
// @public (undocumented)
export const Tabs: {
Root: ForwardRefExoticComponent<
Omit<Tabs_2.Root.Props & RefAttributes<HTMLDivElement>, 'ref'> &
RefAttributes<HTMLDivElement>
>;
List: ForwardRefExoticComponent<
Omit<Tabs_2.List.Props & RefAttributes<HTMLDivElement>, 'ref'> &
RefAttributes<HTMLDivElement>
>;
Tab: ForwardRefExoticComponent<
Omit<Tabs_2.Tab.Props & RefAttributes<Element>, 'ref'> &
RefAttributes<Element>
>;
Panel: ForwardRefExoticComponent<
Omit<Tabs_2.Panel.Props & RefAttributes<HTMLDivElement>, 'ref'> &
RefAttributes<HTMLDivElement>
>;
};
// @public (undocumented)
const Text_2: ForwardRefExoticComponent<
TextProps & RefAttributes<HTMLParagraphElement>
@@ -0,0 +1,50 @@
/*
* 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 { Meta, StoryObj } from '@storybook/react';
import { Tabs } from './Tabs';
import { Text } from '../../index';
const meta = {
title: 'Components/Tabs',
component: Tabs.Root,
} satisfies Meta<typeof Tabs.Root>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: (
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Tab>Tab 3 With long title</Tabs.Tab>
</Tabs.List>
<Tabs.Panel style={{ padding: 'var(--canon-space-3' }}>
<Text>Content for Tab 1</Text>
</Tabs.Panel>
<Tabs.Panel style={{ padding: 'var(--canon-space-3' }}>
<Text>Content for Tab 2</Text>
</Tabs.Panel>
<Tabs.Panel style={{ padding: 'var(--canon-space-3' }}>
<Text>Content for Tab 3</Text>
</Tabs.Panel>
</Tabs.Root>
),
},
};
@@ -0,0 +1,76 @@
.canon-TabsRoot {
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
}
.canon-TabsList {
display: flex;
position: relative;
z-index: 0;
}
.canon-TabsTab {
display: flex;
align-items: center;
justify-content: center;
border: 0;
margin: 0;
outline: 0;
background: none;
appearance: none;
color: var(--canon-fg-secondary);
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
user-select: none;
padding-inline: var(--canon-space-3);
padding-block: 0;
height: 2rem;
font-size: var(--canon-font-size-2);
transition: color 200ms ease-in-out;
cursor: pointer;
&[data-selected] {
color: var(--canon-fg-primary);
}
@media (hover: hover) {
&:hover {
color: var(--canon-fg-primary);
}
}
&:focus-visible {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0.25rem 0;
border-radius: 0.25rem;
outline: 1px solid var(--canon-ring);
outline-offset: -1px;
}
}
}
.canon-TabsIndicator {
position: absolute;
z-index: -1;
left: 0;
bottom: 0;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
height: 1px;
background-color: var(--canon-bg-solid);
transition-property: translate, width;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}
.canon-TabsPanel {
&[hidden] {
display: none;
}
}
@@ -0,0 +1,78 @@
/*
* 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 { forwardRef } from 'react';
import { Tabs as TabsPrimitive } from '@base-ui-components/react/tabs';
import clsx from 'clsx';
const TabsRoot = forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root
ref={ref}
className={clsx('canon-TabsRoot', className)}
{...props}
/>
));
TabsRoot.displayName = TabsPrimitive.Root.displayName;
const TabsList = forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, children, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={clsx('canon-TabsList', className)}
{...props}
>
{children}
<TabsPrimitive.Indicator className="canon-TabsIndicator" />
</TabsPrimitive.List>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTab = forwardRef<
React.ElementRef<typeof TabsPrimitive.Tab>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Tab>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Tab
ref={ref}
className={clsx('canon-TabsTab', className)}
{...props}
/>
));
TabsTab.displayName = TabsPrimitive.Tab.displayName;
const TabsPanel = forwardRef<
React.ElementRef<typeof TabsPrimitive.Panel>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Panel>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Panel
ref={ref}
className={clsx('canon-TabsPanel', className)}
{...props}
/>
));
TabsPanel.displayName = TabsPrimitive.Panel.displayName;
/** @public */
export const Tabs = {
Root: TabsRoot,
List: TabsList,
Tab: TabsTab,
Panel: TabsPanel,
};
@@ -0,0 +1,16 @@
/*
* 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.
*/
export { Tabs } from './Tabs';
+1
View File
@@ -30,6 +30,7 @@
@import '../components/Table/TableCellText/TableCellText.styles.css';
@import '../components/Table/TableCellLink/TableCellLink.styles.css';
@import '../components/Table/TableCellProfile/TableCellProfile.styles.css';
@import '../components/Tabs/Tabs.styles.css';
@import '../components/Text/styles.css';
@import '../components/Heading/styles.css';
@import '../components/IconButton/styles.css';
+1
View File
@@ -40,6 +40,7 @@ export * from './components/Icon';
export * from './components/IconButton';
export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Tabs';
export * from './components/TextField';
export * from './components/Tooltip';
export * from './components/Menu';