Merge pull request #27843 from backstage/canon

Introducing Canon, a new UI library for Backstage
This commit is contained in:
Patrik Oldsberg
2024-11-27 12:57:00 +01:00
committed by GitHub
61 changed files with 6745 additions and 265 deletions
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
...require('@backstage/cli/config/eslint-factory')(__dirname),
extends: ['plugin:storybook/recommended'],
rules: {
'react/forbid-elements': 'off',
},
};
+40
View File
@@ -0,0 +1,40 @@
import type { StorybookConfig } from '@storybook/react-webpack5';
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
import { merge } from 'webpack-merge';
import { join, dirname } from 'path';
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
const config: StorybookConfig = {
stories: [
'../docs/**/*.mdx',
'../src/components/**/*.mdx',
'../src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
staticDirs: ['../static'],
addons: [
getAbsolutePath('@storybook/addon-webpack5-compiler-swc'),
getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-interactions'),
getAbsolutePath('@storybook/addon-themes'),
],
framework: {
name: getAbsolutePath('@storybook/react-webpack5'),
options: {
plugins: [new VanillaExtractPlugin()],
},
},
webpackFinal: config => {
return merge(config, {
plugins: [new VanillaExtractPlugin()],
});
},
};
export default config;
+41
View File
@@ -0,0 +1,41 @@
import React from 'react';
import type { Preview, ReactRenderer } from '@storybook/react';
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import '../src/theme/styles.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
disable: true,
},
},
decorators: [
withThemeByDataAttribute<ReactRenderer>({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
}),
(Story, context) => {
document.body.style.backgroundColor = 'var(--canon-background)';
const docsStoryElements = document.getElementsByClassName('docs-story');
Array.from(docsStoryElements).forEach(element => {
(element as HTMLElement).style.backgroundColor =
'var(--canon-background)';
});
return <Story />;
},
],
};
export default preview;
+12
View File
@@ -0,0 +1,12 @@
# @backstage/canon
_This package was created through the Backstage CLI_.
## Installation
Install the package via Yarn:
```sh
cd <package-dir> # if within a monorepo
yarn add @backstage/canon
```
+9
View File
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-canon
title: '@backstage/canon'
spec:
lifecycle: experimental
type: backstage-web-library
owner: maintainers
+49
View File
@@ -0,0 +1,49 @@
import { Unstyled } from '@storybook/blocks';
import { Columns, Text, ComponentStatus, Banner } from './components';
<Unstyled>
<img src="header.png" style={{ width: '100%', marginTop: '-16px' }} />
<Text style={{ marginTop: '64px' }}>
Welcome to the Canon, the new design library for Backstage plugins. This
project is still under active development but we will make sure to document
the API as we go. We are aiming to improve the general UI of Backstage and
plugins across Backstage. This new library will take time to build but we are
building it incrementally with not conflict with the existing theming system.
</Text>
<Banner variant="warning">
This library is still under heavy construction. Please be aware that the API
will change until we reach a stable release.
</Banner>
<Columns style={{ marginTop: '64px' }}>
<ComponentStatus
name="Theming system"
status="inProgress"
link="/?path=/docs/theme--docs"
/>
<ComponentStatus name="Storybook setup" status="inProgress" />
<ComponentStatus name="Tokens" status="inProgress" />
<ComponentStatus
name="Box"
status="inProgress"
link="/?path=/docs/components-box--docs"
/>
<ComponentStatus
name="Button"
status="inProgress"
link="/?path=/docs/components-button--docs"
/>
<ComponentStatus name="Input" status="notStarted" />
<ComponentStatus name="Select" status="notStarted" />
<ComponentStatus name="Checkbox" status="notStarted" />
<ComponentStatus name="Radio" status="notStarted" />
<ComponentStatus name="Switch" status="notStarted" />
<ComponentStatus name="Tooltip" status="notStarted" />
<ComponentStatus name="Header" status="notStarted" />
<ComponentStatus name="Tabs" status="notStarted" />
</Columns>
</Unstyled>
+47
View File
@@ -0,0 +1,47 @@
import { Unstyled, Source } from '@storybook/blocks';
import { Title, Text, LayoutComponents } from './components';
import * as Table from './components/Table';
<Unstyled>
<Title type="h1">Layout</Title>
<Text>
Canon is made for extensibility. We built this library to make it easy for any
Backstage plugin creator to be able to build their ideas at speed ensuring
consistency across the rest of your ecosystem. Each component is designed to
be editable to match your need but sometimes you want to have more control
over the layout of your page. To help you with that, we created a set of
layout components that you can use to build your own layouts. All of these
components are built to extend on our theming system, making it easy for you
to build your own layouts. Sometimes these components are not enough so we
created a set of helpers to be used with any CSS-in-JS library.
</Text>
<Title type="h2">Layout Components</Title>
<Text>
We built a couple of layout components to help you build responsive elements
that will be consistent with the rest of your Backstage instance. These
components are opinionated and use TypeScript to ensure that the props you
provide are the ones coming from the theme.
</Text>
<Source
code={`<Stack direction="column" gap="md">
<Box>Hello World</Box>
<Inline gap="sm">
<Box>Project 1</Box>
<Box>Project 2</Box>
</Inline>
</Stack>`}
language="tsx"
dark
/>
<LayoutComponents />
<Title type="h2">Layout Helpers</Title>
<Text>TBD</Text>
</Unstyled>
+229
View File
@@ -0,0 +1,229 @@
import { Unstyled, Source } from '@storybook/blocks';
import { Title, Text, Chip } from './components';
import * as Table from './components/Table';
<Unstyled>
<Title type="h1">Theming</Title>
<Text>
Backstage ships with a default theme with a light and dark mode variant. The
themes are provided as a part of the `@backstage/canon` package, which also
includes utilities for customizing the default theme, or creating completely
new themes.
</Text>
<Title type="h2">Light & Dark modes</Title>
<Text>
By default we are supporting both light and dark modes. Each user can opt to
choose what theme they want to use or to use their system decide what theme to
use. If you want to create your own theme, you will have to set both light and
dark themes following the instructions below. If you only set one of them, the
other mode will fallback to the default theme.
</Text>
<Title type="h2">How to create your own theme</Title>
<Text>
To create your own theme, you will have to define the variables below. To do
that, create a theme.css file and import it in your application. Here's an
example below on how to set your light and dark mode.
</Text>
<Source
code={`/** Light theme **/
[data-theme='light'] {
--canon-accent: #1ed760;
--canon-bg: #fff;
--canon-surface-1: #f5f5f5;
--canon-surface-2: #000;
--canon-outline: #666;
--canon-outline-focus: #ccc;
--canon-text-primary: #f0f0f0;
--canon-text-secondary: #666;
--canon-font-sans: 'Geist', serif;
--canon-font-mono: 'Monospace', monospace;
/* ... other values */
}
/** Dark theme **/
[data-theme='dark'] {
--canon-accent: #1ed760;
--canon-bg: #000;
--canon-surface-1: #f5f5f5;
--canon-surface-2: #000;
--canon-outline: #666;
--canon-outline-focus: #ccc;
--canon-text-primary: #fff;
--canon-text-secondary: #666;
--canon-font-sans: 'Geist', serif;
--canon-font-mono: 'Monospace', monospace;
/* ... other values */
}
`}
language="css"
dark
/>
<Title type="h2">Colors</Title>
<Text>
We provide a set of generic colours tokens that we use across Canon. By
changing these colours you can easily change the look and feel of your
application to match your brand.
</Text>
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>--canon-accent</Chip>
</Table.Cell>
<Table.Cell>The accent color for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg</Chip>
</Table.Cell>
<Table.Cell>The background color for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-surface-1</Chip>
</Table.Cell>
<Table.Cell>The first surface color for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-surface-2</Chip>
</Table.Cell>
<Table.Cell>The second surface color for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-outline</Chip>
</Table.Cell>
<Table.Cell>The outline color for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-outline-focus</Chip>
</Table.Cell>
<Table.Cell>The outline focus color for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-text-primary</Chip>
</Table.Cell>
<Table.Cell>The primary text color for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-text-secondary</Chip>
</Table.Cell>
<Table.Cell>The secondary text color for the theme.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
<Title type="h2">Typography</Title>
<Text>
We have two fonts that we use across Canon. The first one is the sans-serif
font that we use for the body of the application. The second one is the
monospace font that we use for code blocks and tables.
</Text>
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>--canon-font-sans</Chip>
</Table.Cell>
<Table.Cell>The sans-serif font for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-font-mono</Chip>
</Table.Cell>
<Table.Cell>The monospace font for the theme.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
<Title type="h2">Spacing</Title>
<Text>
Our default spacing system is made to work in most scenarios. We have 7 scale
values from `xxs` to `xxl`. We use the values on padding and margin in our
layout components mostly. If you prefer to use a different spacing system, you
can do that by changing the values below.
</Text>
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-unit</Chip>
</Table.Cell>
<Table.Cell>
The base unit for the spacing system. Default value is `1em`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-xxs</Chip>
</Table.Cell>
<Table.Cell>Default value is `0.25 x space unit`</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-xs</Chip>
</Table.Cell>
<Table.Cell>Default value is `0.5 x space unit`</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-sm</Chip>
</Table.Cell>
<Table.Cell>Default value is `0.75 x space unit`</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-md</Chip>
</Table.Cell>
<Table.Cell>Default value is `1.25 x space unit`</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-lg</Chip>
</Table.Cell>
<Table.Cell>Default value is `2 x space unit`</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-xl</Chip>
</Table.Cell>
<Table.Cell>Default value is `3.25 x space unit`</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-xxl</Chip>
</Table.Cell>
<Table.Cell>Default value is `5.25 x space unit`</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
</Unstyled>
@@ -0,0 +1,61 @@
/*
* 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 { recipe } from '@vanilla-extract/recipes';
import { globalStyle, style } from '@vanilla-extract/css';
export const bannerStyles = recipe({
base: {
display: 'flex',
alignItems: 'center',
fontFamily: '"Geist", sans-serif',
fontSize: '16px',
lineHeight: '28px',
padding: '16px',
borderRadius: '6px',
marginBottom: '16px',
border: '1px solid #e0e0e0',
},
variants: {
variant: {
info: {
backgroundColor: '#F2F2F2',
borderColor: '#CDCDCD',
color: '#888888',
},
warning: {
backgroundColor: '#FFF2B9',
borderColor: '#FFD000',
color: '#D79927',
},
},
},
});
globalStyle(`${bannerStyles.classNames.base} p`, {
margin: '0',
});
export const bannerIconStyles = style({
width: '32px',
height: '32px',
backgroundColor: 'rgba(215, 153, 39, 0.2)',
borderRadius: '6px',
marginRight: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
});
@@ -0,0 +1,42 @@
/*
* 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 { bannerIconStyles, bannerStyles } from './banner.css';
export const Banner = ({
children,
variant = 'info',
}: {
children: React.ReactNode;
variant?: 'info' | 'warning';
}) => {
return (
<div className={bannerStyles({ variant })}>
<div className={bannerIconStyles}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="18"
height="18"
fill="currentColor"
>
<path d="M4.00001 20V14C4.00001 9.58172 7.58173 6 12 6C16.4183 6 20 9.58172 20 14V20H21V22H3.00001V20H4.00001ZM6.00001 20H18V14C18 10.6863 15.3137 8 12 8C8.6863 8 6.00001 10.6863 6.00001 14V20ZM11 2H13V5H11V2ZM19.7782 4.80761L21.1924 6.22183L19.0711 8.34315L17.6569 6.92893L19.7782 4.80761ZM2.80762 6.22183L4.22183 4.80761L6.34315 6.92893L4.92894 8.34315L2.80762 6.22183ZM7.00001 14C7.00001 11.2386 9.23858 9 12 9V11C10.3432 11 9.00001 12.3431 9.00001 14H7.00001Z"></path>
</svg>
</div>
{children}
</div>
);
};
@@ -0,0 +1,42 @@
/*
* 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 { recipe } from '@vanilla-extract/recipes';
export const chipStyles = recipe({
base: {
display: 'inline-flex',
alignItems: 'center',
fontFamily: 'Geist Mono, monospace',
fontSize: '13px',
borderRadius: '6px',
padding: '0px 8px',
height: '24px',
marginRight: '4px',
},
variants: {
head: {
true: {
backgroundColor: '#eaf2fd',
color: '#2563eb',
},
false: {
backgroundColor: '#f0f0f0',
color: '#5d5d5d',
},
},
},
});
@@ -0,0 +1,28 @@
/*
* 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 { chipStyles } from './chip.css';
export const Chip = ({
children,
head = false,
}: {
children: React.ReactNode;
head?: boolean;
}) => {
return <span className={chipStyles({ head })}>{children}</span>;
};
@@ -0,0 +1,24 @@
/*
* 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 { style } from '@vanilla-extract/css';
export const styles = style({
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
rowGap: '20px',
columnGap: '80px',
});
@@ -0,0 +1,32 @@
/*
* 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 { styles } from './columns.css';
export const Columns = ({
children,
style,
}: {
children: React.ReactNode;
style?: React.CSSProperties;
}) => {
return (
<div className={styles} style={style}>
{children}
</div>
);
};
@@ -0,0 +1,67 @@
/*
* 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 { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
export const container = style({
display: 'flex',
justifyContent: 'space-between',
});
export const title = style({
color: '#3b59ff',
fontFamily: '"Geist", sans-serif',
fontSize: '16px',
fontWeight: 400,
textDecoration: 'none',
});
export const pill = recipe({
base: {
fontFamily: '"Geist", sans-serif',
display: 'inline-flex',
alignItems: 'center',
color: '#000',
borderRadius: '40px',
padding: '0px 8px',
height: '24px',
fontSize: '12px',
fontWeight: 600,
borderStyle: 'solid',
borderWidth: '1px',
marginLeft: '8px',
},
variants: {
status: {
notStarted: {
backgroundColor: '#F2F2F2',
borderColor: '#CDCDCD',
color: '#888888',
},
inProgress: {
backgroundColor: '#FFF2B9',
borderColor: '#FFD000',
color: '#D79927',
},
done: {
backgroundColor: '#D7F9D7',
borderColor: '#4ED14A',
color: '#3A9837',
},
},
},
});
@@ -0,0 +1,47 @@
/*
* 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 { container, pill, title } from './component-status.css';
export const ComponentStatus = ({
name,
status = 'notStarted',
style,
link,
}: {
name: string;
status: 'notStarted' | 'inProgress' | 'done';
style?: React.CSSProperties;
link?: string;
}) => {
return (
<div className={container} style={style}>
{link ? (
<a href={link} className={title}>
{name}
</a>
) : (
<span className={title}>{name}</span>
)}
<span className={pill({ status })}>
{status === 'notStarted' && 'Not Started'}
{status === 'inProgress' && 'In Progress'}
{status === 'done' && 'Done'}
</span>
</div>
);
};
@@ -0,0 +1,109 @@
/*
* 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 {
container,
box,
content,
title,
description,
whiteBox,
whiteBoxStack,
stack,
verticalDivider,
horizontalDivider,
columns,
inline,
tiles,
whiteBoxColumns,
whiteBoxInline,
whiteBoxTiles,
} from './layout-components.css';
export const LayoutComponents = () => {
return (
<div className={container}>
<div className={box}>
<a className={content} href="/?path=/docs/components-box--docs">
<div className={whiteBox} />
</a>
<div className={title}>Box</div>
<div className={description}>The most basic layout component.</div>
</div>
<div className={box}>
<a
className={`${content} ${stack}`}
href="/?path=/docs/components-stack--docs"
>
<div className={whiteBoxStack} />
<div className={verticalDivider} />
<div className={whiteBoxStack} />
</a>
<div className={title}>Stack</div>
<div className={description}>Arrange your components vertically.</div>
</div>
<div className={box}>
<a
className={`${content} ${columns}`}
href="/?path=/docs/components-columns--docs"
>
<div className={whiteBoxColumns} />
<div className={horizontalDivider} />
<div className={whiteBoxColumns} />
</a>
<div className={title}>Columns</div>
<div className={description}>Arrange your components horizontally.</div>
</div>
<div className={box}>
<a className={content} href="/?path=/docs/components-inline--docs">
<div className={inline}>
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
<div className={whiteBoxInline} />
</div>
</a>
<div className={title}>Inline</div>
<div className={description}>Arrange your components in a row.</div>
</div>
<div className={box}>
<a className={content} href="/?path=/docs/components-tiles--docs">
<div className={tiles}>
<div className={whiteBoxTiles} />
<div className={whiteBoxTiles} />
<div className={whiteBoxTiles} />
<div className={whiteBoxTiles} />
<div className={whiteBoxTiles} />
<div className={whiteBoxTiles} />
<div className={whiteBoxTiles} />
<div className={whiteBoxTiles} />
</div>
</a>
<div className={title}>Tiles</div>
<div className={description}>Arrange your components in a grid.</div>
</div>
</div>
);
};
@@ -0,0 +1,198 @@
/*
* 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 { style } from '@vanilla-extract/css';
export const container = style({
display: 'flex',
justifyContent: 'flex-start',
gap: '1rem',
flexWrap: 'wrap',
});
export const box = style({
display: 'flex',
flexDirection: 'column',
width: 'calc(33.33% - 0.67rem)',
marginBottom: '1rem',
alignItems: 'flex-start',
});
export const content = style({
flex: 'none',
background: '#f2f2f2',
borderRadius: '4px',
width: '100%',
height: '180px',
border: '1px solid #e0e0e0',
transition: 'border-color 0.2s ease-in-out',
marginBottom: '0.75rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
selectors: {
[`${box}:hover &`]: {
borderColor: '#1f47ff',
},
},
});
export const title = style({
fontSize: '16px',
fontFamily: 'var(--canon-font-sans)',
transition: 'color 0.2s ease-in-out',
marginBottom: '0.25rem',
selectors: {
[`${box}:hover &`]: {
color: '#1f47ff',
},
},
});
export const description = style({
fontSize: '16px',
fontFamily: 'var(--canon-font-sans)',
color: '#9e9e9e',
});
export const whiteBox = style({
background: '#fff',
width: '40px',
height: '40px',
borderRadius: '4px',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
});
export const whiteBoxStack = style([
whiteBox,
{
width: '40%',
height: '28px',
},
]);
export const whiteBoxColumns = style([
whiteBox,
{
width: '30%',
height: '32px',
},
]);
export const whiteBoxInline = style([
whiteBox,
{
width: '32px',
height: '32px',
},
]);
export const whiteBoxTiles = style([
whiteBox,
{
width: '100%',
height: '32px',
},
]);
export const stack = style({
display: 'flex',
flexDirection: 'column',
});
export const columns = style({
display: 'flex',
flexDirection: 'row',
});
export const inline = style({
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'flex-start',
gap: '0.5rem',
paddingLeft: '1rem',
});
export const tiles = style({
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '0.5rem',
paddingLeft: '1rem',
paddingRight: '1rem',
width: '100%',
});
export const verticalDivider = style({
display: 'flex',
flexShrink: 0,
flexDirection: 'column',
position: 'relative',
height: '20px',
width: '2px',
backgroundColor: '#1f47ff',
selectors: {
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-9px',
width: '20px',
height: '2px',
backgroundColor: '#1f47ff',
},
'&::after': {
content: '""',
position: 'absolute',
bottom: 0,
left: '-9px',
width: '20px',
height: '2px',
backgroundColor: '#1f47ff',
},
},
});
export const horizontalDivider = style({
flexShrink: 0,
display: 'flex',
flexDirection: 'row',
position: 'relative',
height: '2px',
width: '20px',
backgroundColor: '#1f47ff',
selectors: {
'&::before': {
content: '""',
position: 'absolute',
left: 0,
top: '-9px',
width: '2px',
height: '20px',
backgroundColor: '#1f47ff',
},
'&::after': {
content: '""',
position: 'absolute',
right: 0,
top: '-9px',
width: '2px',
height: '20px',
backgroundColor: '#1f47ff',
},
},
});
@@ -0,0 +1,56 @@
/*
* 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 {
cellStyles,
headerCellStyles,
tableStyles,
wrapperStyles,
rowStyles,
} from './table.css';
export const Root = ({ children }: { children: React.ReactNode }) => {
return (
<div className={wrapperStyles}>
<table className={tableStyles}>{children}</table>
</div>
);
};
export const Header = ({ children }: { children: React.ReactNode }) => {
return <thead>{children}</thead>;
};
export const Body = ({ children }: { children: React.ReactNode }) => {
return <tbody>{children}</tbody>;
};
export const HeaderRow = ({ children }: { children: React.ReactNode }) => {
return <tr>{children}</tr>;
};
export const HeaderCell = ({ children }: { children: React.ReactNode }) => {
return <th className={`${cellStyles} ${headerCellStyles}`}>{children}</th>;
};
export const Row = ({ children }: { children: React.ReactNode }) => {
return <tr className={rowStyles}>{children}</tr>;
};
export const Cell = ({ children }: { children: React.ReactNode }) => {
return <td className={cellStyles}>{children}</td>;
};
@@ -0,0 +1,83 @@
/*
* 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 { style, globalStyle } from '@vanilla-extract/css';
export const wrapperStyles = style({
border: '1px solid #e7e7e7',
borderRadius: '4px',
overflow: 'hidden',
});
export const tableStyles = style({
width: '100%',
margin: '0 !important',
padding: '0 !important',
borderSpacing: '0px',
borderCollapse: 'collapse',
});
export const cellStyles = style({
padding: '12px 16px !important',
border: 'none !important',
textAlign: 'left',
backgroundColor: 'white !important',
fontFamily: '"Geist", sans-serif',
fontSize: '16px',
fontWeight: 300,
});
// Apply global styles to <p> elements inside cellStyles
globalStyle(`${cellStyles} p`, {
margin: '0',
});
export const headerRowStyles = style({
borderBottom: '1px solid #e7e7e7',
});
export const headerCellStyles = style({
backgroundColor: '#f5f5f5 !important',
borderBottom: '1px solid #e7e7e7 !important',
fontWeight: 500,
fontSize: '14px',
});
export const rowStyles = style({
border: 'none',
borderBottom: '1px solid #e7e7e7',
selectors: {
'&:last-child': {
borderBottom: 'none',
},
},
});
export const chipStyles = style({
display: 'inline-block',
fontSize: '14px !important',
border: '1px solid #e7e7e7',
borderRadius: '6px',
padding: '0px 6px',
height: '24px',
});
export const typeStyles = style({
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
gap: '8px',
});
@@ -0,0 +1,32 @@
/*
* 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 { textStyles } from './text.css';
export const Text = ({
children,
style,
}: {
children: React.ReactNode;
style?: React.CSSProperties;
}) => {
return (
<div className={textStyles} style={style}>
{children}
</div>
);
};
@@ -0,0 +1,34 @@
/*
* 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 { globalStyle, style } from '@vanilla-extract/css';
export const textStyles = style({
fontFamily: '"Geist", sans-serif',
fontSize: '16px',
lineHeight: '28px',
margin: '0',
fontWeight: 300,
paddingBottom: '16px',
});
globalStyle(`${textStyles} p`, {
fontFamily: '"Geist", sans-serif',
fontSize: '16px',
lineHeight: '28px',
margin: '0',
fontWeight: 300,
});
@@ -0,0 +1,39 @@
/*
* 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 { titleStyles } from './title.css';
export const Title = ({
children,
style,
type = 'h1',
}: {
children: React.ReactNode;
style?: React.CSSProperties;
type?: 'h1' | 'h2' | 'h3';
}) => {
let Component = 'h1';
if (type === 'h1') Component = 'h1';
if (type === 'h2') Component = 'h2';
if (type === 'h3') Component = 'h3';
return React.createElement(Component, {
className: titleStyles({ type }),
style,
children,
});
};
@@ -0,0 +1,48 @@
/*
* 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 { recipe } from '@vanilla-extract/recipes';
export const titleStyles = recipe({
base: {
fontFamily: '"Geist", sans-serif',
margin: '0',
fontWeight: 500,
border: 'none',
},
variants: {
type: {
h1: {
fontSize: '36px',
lineHeight: '44px',
marginBottom: '32px',
marginTop: '0px',
},
h2: {
fontSize: '24px',
lineHeight: '32px',
marginBottom: '12px',
marginTop: '52px',
},
h3: {
fontSize: '20px',
lineHeight: '28px',
marginBottom: '12px',
marginTop: '52px',
},
},
},
});
+24
View File
@@ -0,0 +1,24 @@
/*
* 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 * from './Chip';
export * from './Table';
export * from './Title';
export * from './Text';
export * from './Columns';
export * from './ComponentStatus';
export * from './LayoutComponents';
export * from './Banner';
+84
View File
@@ -0,0 +1,84 @@
{
"name": "@backstage/canon",
"version": "0.0.0",
"backstage": {
"role": "web-library"
},
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"private": true,
"keywords": [
"backstage"
],
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/canon"
},
"license": "Apache-2.0",
"sideEffects": false,
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"build-storybook": "storybook build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"start": "storybook dev -p 6006",
"test": "backstage-cli package test"
},
"dependencies": {
"@base_ui/react": "^1.0.0-alpha.3",
"@vanilla-extract/css": "^1.16.0",
"@vanilla-extract/dynamic": "^2.1.2",
"@vanilla-extract/recipes": "^0.5.5",
"@vanilla-extract/sprinkles": "^1.6.3",
"lucide-react": "^0.460.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@chromatic-com/storybook": "^3.2.2",
"@storybook/addon-essentials": "^8.4.5",
"@storybook/addon-interactions": "^8.4.5",
"@storybook/addon-styling-webpack": "^1.0.1",
"@storybook/addon-themes": "^8.4.5",
"@storybook/addon-webpack5-compiler-swc": "^1.0.5",
"@storybook/blocks": "^8.4.5",
"@storybook/react": "^8.4.5",
"@storybook/react-webpack5": "^8.4.5",
"@storybook/test": "^8.4.5",
"@testing-library/jest-dom": "^6.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@vanilla-extract/rollup-plugin": "^1.3.10",
"@vanilla-extract/webpack-plugin": "^2.3.14",
"eslint-plugin-storybook": "^0.11.1",
"globals": "^15.11.0",
"mini-css-extract-plugin": "^2.9.2",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.3.0",
"storybook": "^8.4.5",
"webpack-merge": "^6.0.1"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,229 @@
/*
* 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 { Box } from './Box';
import { listResponsiveValues } from '../../utils/list-values';
import { responsiveProperties, colorProperties } from './sprinkles.css';
const argTypesResponsive = Object.keys(responsiveProperties.styles).reduce<
Record<string, any>
>((acc, n) => {
if (
[
'margin',
'marginBottom',
'marginLeft',
'marginRight',
'marginTop',
'marginX',
'marginY',
'padding',
'paddingBottom',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingX',
'paddingY',
].includes(n)
) {
acc[n] = {
control: 'select',
options: ['xs', 'sm', 'md', 'lg', 'xl', '2xl'],
};
} else {
acc[n] = {
control: 'select',
options: listResponsiveValues(
n as keyof typeof responsiveProperties.styles,
),
};
}
return acc;
}, {});
const argTypesColor = Object.keys(colorProperties.styles).reduce<
Record<string, any>
>((acc, n) => {
acc[n as keyof typeof colorProperties.styles] = {
control: 'select',
options: Object.keys(
colorProperties.styles[n as keyof typeof colorProperties.styles].values,
),
};
return acc;
}, {});
const argTypes = {
...argTypesResponsive,
...argTypesColor,
};
// Sort the resulting object keys alphabetically
const sortedArgTypes = Object.keys(argTypes)
.sort()
.reduce((acc, key) => {
acc[key] = argTypes[key];
return acc;
}, {} as Record<string, any>);
// Add 'as' and 'children' to the sortedArgTypes
sortedArgTypes['as'] = {
control: 'select',
options: ['div', 'span', 'article', 'section'],
};
sortedArgTypes['children'] = {
control: 'text',
};
const meta = {
title: 'Components/Box',
component: Box,
parameters: {
layout: 'centered',
},
argTypes: sortedArgTypes,
args: {
as: 'div',
background: 'elevation1',
borderRadius: 'small',
children: 'Basic Box',
display: 'block',
padding: 'sm',
},
} satisfies Meta<typeof Box>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {},
};
export const Responsive: Story = {
render: () => (
<Box
display={{ xs: 'block', sm: 'flex' }}
padding={{ xs: 'xs', sm: 'md', lg: 'lg' }}
background="background"
>
Responsive Box
</Box>
),
};
export const FlexContainer: Story = {
render: () => (
<Box
display="flex"
flexDirection="column"
padding="lg"
background="background"
>
<Box padding="md" background="background" color="primary">
Item 1
</Box>
<Box padding="md" background="background" color="primary">
Item 2
</Box>
<Box padding="md" background="background" color="primary">
Item 3
</Box>
</Box>
),
};
export const Nested: Story = {
render: () => (
<Box padding="lg" background="background">
<Box padding="md" background="background">
Header
</Box>
<Box
display="flex"
padding="md"
background="background"
justifyContent="space-between"
>
<Box padding="xs" background="background">
Sidebar
</Box>
<Box padding="xs" background="background">
Main Content
</Box>
</Box>
<Box padding="md" background="background">
Footer
</Box>
</Box>
),
};
export const Alignment: Story = {
render: () => (
<Box
display="flex"
padding="lg"
background="background"
justifyContent="center"
alignItems="center"
style={{ height: '200px' }}
>
<Box padding="md" background="background">
Centered Content
</Box>
</Box>
),
};
// Example showing different spacing combinations
export const Spacing: Story = {
render: () => (
<Box display="flex" flexDirection="column" gap="md">
<Box padding="xs" background="background">
Small Padding
</Box>
<Box padding="md" background="background">
Medium Padding
</Box>
<Box padding="lg" background="background">
Large Padding
</Box>
<Box paddingX="lg" paddingY="xs" background="background">
Mixed Padding
</Box>
</Box>
),
};
// Example showing different display values
export const DisplayVariants: Story = {
render: () => (
<Box display="flex" flexDirection="column" gap="md">
<Box padding="md" background="background" display="block">
Display Block
</Box>
<Box padding="md" background="background" display="flex">
Display Flex
</Box>
<Box padding="md" background="background" display="inline">
Display Inline
</Box>
</Box>
),
};
+62
View File
@@ -0,0 +1,62 @@
/*
* 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 { createElement } from 'react';
import { boxSprinkles } from './sprinkles.css';
import { base } from './box.css';
/**
* Properties for {@link Box}
*
* @public
*/
export type BoxProps = Parameters<typeof boxSprinkles>[0] &
Omit<
React.AllHTMLAttributes<HTMLElement>,
keyof Parameters<typeof boxSprinkles>[0]
> & {
as?: keyof JSX.IntrinsicElements;
};
/** @public */
export const Box = (props: BoxProps) => {
const { as = 'div', className, style, ...rest } = props;
const boxSprinklesProps: Record<string, unknown> = {};
const nativeProps: Record<string, unknown> = {};
// Split props between sprinkles and native HTML props
Object.entries(rest).forEach(([key, value]) => {
if (value === undefined) return;
if (
boxSprinkles.properties.has(
key as keyof Parameters<typeof boxSprinkles>[0],
)
) {
boxSprinklesProps[key] = value;
} else {
nativeProps[key] = value;
}
});
const sprinklesClassName = boxSprinkles(boxSprinklesProps);
return createElement(as, {
className: [base, sprinklesClassName, className].filter(Boolean).join(' '),
style,
...nativeProps,
});
};
@@ -0,0 +1,22 @@
/*
* 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 { style } from '@vanilla-extract/css';
export const base = style({
fontFamily: 'var(--canon-font-sans)',
color: 'var(--canon-text-primary)',
});
+38
View File
@@ -0,0 +1,38 @@
import { Canvas, Meta, Unstyled } from '@storybook/blocks';
import * as BoxStories from './Box.stories';
import { Chip } from '../../../docs/components/Chip';
import * as Table from '../../../docs/components/Table';
import { Box } from './Box';
import { Stack } from '../Stack';
import { Title, Text } from '../../../docs/components';
import { Padding } from './docs/padding';
import { listResponsiveValues, listColorValues } from '../../utils/list-values';
import { responsiveProperties } from './sprinkles.css';
import { PropsTable } from './docs/props-table';
import { SpacingTable } from './docs/spacing-table';
<Meta of={BoxStories} />
<Unstyled>
<Title type="h1">Box</Title>
<Text>
Box is the lowest-level component in Canon. We use it internally to build all
of our components. It is a wrapper around the HTML `<div>` element and
provides a consistent API for styling and layout. You'll find below a list of all the available properties.
</Text>
<PropsTable />
<Title type="h2">Padding & Margin</Title>
<Text>
Padding and margin are used to create space around your component using our
predefined spacing tokens. We would recommend to use padding over margin to
avoid collapsing margins but both are available.
</Text>
<Padding />
<SpacingTable />
</Unstyled>
@@ -0,0 +1,105 @@
/*
* 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 { Stack } from '../../Stack';
import { Inline } from '../../Inline';
import { Box } from '../Box';
const FakeBox = ({ children }: { children: string }) => (
<Box
paddingX="md"
paddingY="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
{children}
</Box>
);
export const Padding = () => {
return (
<Stack
align="center"
borderRadius="small"
marginBottom="md"
gap="xl"
paddingY="xl"
style={{ border: '1px solid #e7e7e7' }}
>
<Inline align="center" gap="xl">
<Stack
align="center"
borderRadius="small"
padding="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>padding</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
paddingX="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingX</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
paddingY="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingY</FakeBox>
</Stack>
</Inline>
<Inline align="center" gap="xl">
<Stack
align="center"
borderRadius="small"
paddingTop="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingTop</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
paddingBottom="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingBottom</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
paddingLeft="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingLeft</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
paddingRight="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingRight</FakeBox>
</Stack>
</Inline>
</Stack>
);
};
@@ -0,0 +1,70 @@
/*
* 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 * as Table from '../../../../docs/components';
import { Chip } from '../../../../docs/components';
import { listResponsiveValues } from '../../../utils/list-values';
import { responsiveProperties } from '../sprinkles.css';
export const PropsTable = () => {
return (
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Type</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
{Object.keys(responsiveProperties.styles)
.filter(
n =>
![
'padding',
'paddingX',
'paddingY',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'margin',
'marginX',
'marginY',
'marginLeft',
'marginRight',
'marginTop',
'marginBottom',
].includes(n),
)
.map(n => (
<Table.Row key={n}>
<Table.Cell>
<Chip head>{n}</Chip>
</Table.Cell>
<Table.Cell>
{listResponsiveValues(
n as keyof typeof responsiveProperties.styles,
).map(value => (
<Chip key={value}>{value}</Chip>
))}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};
@@ -0,0 +1,71 @@
/*
* 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 * as Table from '../../../../docs/components';
import { Chip } from '../../../../docs/components';
import { listResponsiveValues } from '../../../utils/list-values';
import { responsiveProperties } from '../sprinkles.css';
console.log(responsiveProperties.styles);
export const SpacingTable = () => {
return (
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Type</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
{Object.keys(responsiveProperties.styles)
.filter(n =>
[
'padding',
'paddingX',
'paddingY',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'margin',
'marginX',
'marginY',
'marginLeft',
'marginRight',
'marginTop',
'marginBottom',
].includes(n),
)
.map(n => (
<Table.Row key={n}>
<Table.Cell>
<Chip head>{n}</Chip>
</Table.Cell>
<Table.Cell>
{listResponsiveValues(
'paddingTop' as keyof typeof responsiveProperties.styles,
).map(value => (
<Chip key={value}>{value}</Chip>
))}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};
@@ -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 { Box } from './Box';
export type { BoxProps } from './Box';
export { boxSprinkles } from './sprinkles.css';
@@ -0,0 +1,35 @@
/*
* 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 const breakpoints = {
xs: {},
sm: { '@media': 'screen and (min-width: 640px)' },
md: { '@media': 'screen and (min-width: 768px)' },
lg: { '@media': 'screen and (min-width: 1024px)' },
xl: { '@media': 'screen and (min-width: 1280px)' },
'2xl': { '@media': 'screen and (min-width: 1536px)' },
};
export const space = {
none: 0,
xxs: 'var(--canon-space-xxs)',
xs: 'var(--canon-space-xs)',
sm: 'var(--canon-space-sm)',
md: 'var(--canon-space-md)',
lg: 'var(--canon-space-lg)',
xl: 'var(--canon-space-xl)',
xxl: 'var(--canon-space-xxl)',
};
@@ -0,0 +1,112 @@
/*
* 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 {
defineProperties,
createSprinkles,
RequiredConditionalValue,
ConditionalValue,
createMapValueFn,
} from '@vanilla-extract/sprinkles';
import { breakpoints, space } from './properties';
export const responsiveProperties = defineProperties({
conditions: breakpoints,
defaultCondition: 'xs',
responsiveArray: ['xs', 'sm', 'md', 'lg', 'xl', '2xl'],
properties: {
flexDirection: ['row', 'column'],
justifyContent: [
'stretch',
'flex-start',
'center',
'flex-end',
'space-around',
'space-between',
],
alignItems: ['stretch', 'flex-start', 'center', 'flex-end'],
borderRadius: {
none: 0,
small: '4px',
medium: '8px',
full: '9999px',
},
boxShadow: {
small: 'var(--canon-box-shadow-small)',
medium: 'var(--canon-box-shadow-medium)',
large: 'var(--canon-box-shadow-large)',
},
border: {
none: 'none',
thin: '1px solid var(--canon-outline)',
error: '1px solid var(--canon-error)',
},
display: ['none', 'flex', 'block', 'inline'],
paddingTop: space,
paddingBottom: space,
paddingLeft: space,
paddingRight: space,
marginTop: space,
marginBottom: space,
marginLeft: space,
marginRight: space,
gap: space,
flexWrap: ['wrap', 'nowrap'],
},
shorthands: {
padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
paddingX: ['paddingLeft', 'paddingRight'],
paddingY: ['paddingTop', 'paddingBottom'],
margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
marginX: ['marginLeft', 'marginRight'],
marginY: ['marginTop', 'marginBottom'],
},
});
export const colorProperties = defineProperties({
conditions: {
light: { selector: '[data-theme="light"] &' },
dark: { selector: '[data-theme="dark"] &' },
},
defaultCondition: ['light', 'dark'],
properties: {
color: {
primary: 'var(--canon-text-primary)',
secondary: 'var(--canon-text-secondary)',
error: 'var(--canon-error)',
},
background: {
background: 'var(--canon-background)',
elevation1: 'var(--canon-surface-1)',
elevation2: 'var(--canon-surface-2)',
transparent: 'transparent',
},
},
});
/** @public */
export const boxSprinkles = createSprinkles(
responsiveProperties,
colorProperties,
);
export type OptionalResponsiveValue<Value extends string | number> =
ConditionalValue<typeof responsiveProperties, Value>;
export type RequiredResponsiveValue<Value extends string | number> =
RequiredConditionalValue<typeof responsiveProperties, Value>;
export const mapResponsiveValue = createMapValueFn(responsiveProperties);
@@ -0,0 +1,121 @@
/*
* 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 { Button } from './Button';
import { Box } from '../Box/Box';
const meta = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
size: {
control: 'select',
options: ['small', 'medium'],
},
},
args: {
size: 'medium',
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: 'Primary button',
},
};
export const Secondary: Story = {
args: {
children: 'Secondary button',
variant: 'secondary',
},
};
export const Sizes: Story = {
args: {
children: 'Button',
},
render: args => (
<div style={{ display: 'flex', gap: '10px' }}>
<Button {...args} size="small" />
<Button {...args} size="medium" />
</div>
),
};
export const WithIcons: Story = {
args: {
children: 'Button',
},
render: args => (
<div style={{ display: 'flex', gap: '10px' }}>
<Button {...args} iconStart="Cloud" />
<Button {...args} iconEnd="ArrowRight" />
<Button {...args} iconStart="Cloud" iconEnd="ArrowRight" />
</div>
),
};
export const FullWidth: Story = {
args: {
children: 'Button',
},
render: args => (
<Box>
<Button {...args} iconStart="Cloud" />
<Button {...args} iconEnd="ArrowRight" />
<Button {...args} iconStart="Cloud" iconEnd="ArrowRight" />
</Box>
),
};
export const Disabled: Story = {
args: {
children: 'Button',
disabled: true,
},
};
export const CustomTheme: Story = {
args: {
children: 'Custom Button',
},
decorators: [
Story => (
<div
style={
{
'--button-primary-background-color': 'blue',
'--button-primary-border-color': 'blue',
'--button-primary-text-color': 'white',
'--button-primary-border-radius': '8px',
} as React.CSSProperties
}
>
<Story />
</div>
),
],
};
@@ -0,0 +1,61 @@
/*
* 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 { button } from './button.css';
import { Box } from '../Box/Box';
import { Icon } from '../Icon/Icon';
import { IconNames } from '../Icon/context';
/**
* Properties for {@link Button}
*
* @public
*/
export interface ButtonProps {
size?: 'small' | 'medium';
variant?: 'primary' | 'secondary';
children: React.ReactNode;
disabled?: boolean;
iconStart?: IconNames;
iconEnd?: IconNames;
}
/** @public */
export const Button = ({
size = 'medium',
variant = 'primary',
children,
disabled,
iconStart,
iconEnd,
...props
}: ButtonProps) => {
return (
<Box
{...props}
as="button"
disabled={disabled}
className={button({ size, variant, disabled })}
>
{iconStart && <Icon name={iconStart} />}
{children}
{iconEnd && <Icon name={iconEnd} />}
</Box>
);
};
export default Button;
@@ -0,0 +1,75 @@
/*
* 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 { recipe } from '@vanilla-extract/recipes';
export const button = recipe({
base: {
all: 'unset',
display: 'inline-flex',
alignItems: 'center',
userSelect: 'none',
fontFamily: 'var(--canon-font-sans)',
fontWeight: 600,
fontSize: '0.875rem',
lineHeight: '1.5',
padding: 0,
transition: 'all 150ms ease',
cursor: 'pointer',
},
variants: {
variant: {
primary: {
backgroundColor: 'var(--button-primary-background-color)',
color: 'var(--button-primary-text-color)',
borderRadius: 'var(--button-primary-border-radius)',
border: `1px solid var(--button-primary-border-color)`,
'&:hover': {
backgroundColor: 'var(--button-primary-background-color-hover)',
borderColor: 'var(--button-primary-border-color-hover)',
},
},
secondary: {
backgroundColor: 'transparent',
color: 'var(--button-secondary-text-color)',
borderRadius: 'var(--button-secondary-border-radius)',
border: `1px solid var(--button-secondary-border-color)`,
'&:hover': {
backgroundColor: 'var(--button-secondary-background-color-hover)',
borderColor: 'var(--button-secondary-border-color-hover)',
},
},
},
size: {
small: {
paddingLeft: '16px',
paddingRight: '16px',
height: 'var(--button-size-small)',
},
medium: {
paddingLeft: '32px',
paddingRight: '32px',
height: 'var(--button-size-medium)',
},
},
disabled: {
true: {
opacity: 0.3,
cursor: 'not-allowed',
},
},
},
});
@@ -0,0 +1,17 @@
/*
* 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 { Button } from './Button';
export type { ButtonProps } from './Button';
@@ -0,0 +1,57 @@
/*
* 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.
*/
:root {
--button-primary-background-color: #1ed760;
--button-primary-background-color-hover: #3be477;
--button-primary-background-color-active: #1abc54;
--button-primary-border-color: #1ed760;
--button-primary-border-color-hover: #3be477;
--button-primary-border-color-active: #1abc54;
--button-primary-text-color: #000;
--button-primary-border-radius: 9999px;
--button-secondary-background-color: transparent;
--button-secondary-background-color-hover: transparent;
--button-secondary-background-color-active: rgba(0, 0, 0, 0.03);
--button-secondary-border-color: #000;
--button-secondary-border-color-hover: #000;
--button-secondary-border-color-active: #000;
--button-secondary-text-color: #000;
--button-secondary-border-radius: 9999px;
--button-size-small: 32px;
--button-size-medium: 48px;
}
[data-theme='dark'] {
--button-primary-background-color: #1ed760;
--button-primary-background-color-hover: #3be477;
--button-primary-background-color-active: #1abc54;
--button-primary-border-color: #1ed760;
--button-primary-border-color-hover: #3be477;
--button-primary-border-color-active: #1abc54;
--button-primary-text-color: #000;
--button-primary-border-radius: 9999px;
--button-secondary-background-color: transparent;
--button-secondary-background-color-hover: transparent;
--button-secondary-background-color-active: rgba(255, 255, 255, 0.1);
--button-secondary-border-color: #fff;
--button-secondary-border-color-hover: #fff;
--button-secondary-border-color-active: #fff;
--button-secondary-text-color: #fff;
--button-secondary-border-radius: 9999px;
--button-size-small: 32px;
--button-size-medium: 48px;
}
@@ -0,0 +1,82 @@
/*
* 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 { Meta, StoryObj } from '@storybook/react';
import { Icon } from './Icon';
import { IconProvider } from './context';
import * as LucideIcons from 'lucide-react';
const meta = {
title: 'Components/Icon',
component: Icon,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
name: {
control: 'select',
options: Object.keys(LucideIcons),
},
},
args: {
name: 'ArrowDown',
},
} satisfies Meta<typeof Icon>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
name: 'ArrowDown',
},
};
export const CustomIcon: Story = {
args: {
name: 'CustomIcon',
},
};
export const WithCustomIcon: Story = {
args: {
name: 'ArrowDown',
},
decorators: [
Story => (
<IconProvider overrides={{ ArrowDown: () => <div>Custom Icon</div> }}>
<Story />
</IconProvider>
),
],
};
export const WithCustomIconOverride: Story = {
args: {
name: 'CustomIcon',
},
decorators: [
Story => (
<IconProvider
overrides={{ CustomIcon: () => <div>Custom Super Icon</div> }}
>
<Story />
</IconProvider>
),
],
};
@@ -0,0 +1,32 @@
/*
* 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 { useIcons, IconNames } from './context';
/** @public */
export const Icon = ({ name }: { name: IconNames }) => {
const { icons } = useIcons();
const LucideIcon = icons[name];
if (!LucideIcon) {
console.error(`Icon "${name}" not found.`);
return <svg />; // Return default icon perhaps?
}
return <LucideIcon />;
};
@@ -0,0 +1,68 @@
/*
* 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, { createContext, useContext, ReactNode } from 'react';
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Cloud } from 'lucide-react';
import { CustomIcon } from './custom-icon';
// List of icons available that can also be overridden.
/** @public */
export type IconNames =
| 'ArrowDown'
| 'ArrowLeft'
| 'ArrowRight'
| 'ArrowUp'
| 'Cloud'
| 'CustomIcon';
type IconMap = Partial<Record<IconNames, React.ComponentType>>;
interface IconContextProps {
icons: IconMap;
}
// Create a default icon map with only the necessary icons
const defaultIcons: IconMap = {
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
Cloud,
CustomIcon,
};
const IconContext = createContext<IconContextProps>({ icons: defaultIcons });
/** @public */
export const IconProvider = ({
children,
overrides,
}: {
children: ReactNode;
overrides: Partial<Record<IconNames, React.ComponentType>>;
}) => {
// Merge provided overrides with default icons
const combinedIcons = { ...defaultIcons, ...overrides };
return (
<IconContext.Provider value={{ icons: combinedIcons }}>
{children}
</IconContext.Provider>
);
};
/** @public */
export const useIcons = () => useContext(IconContext);
@@ -0,0 +1,23 @@
/*
* 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';
export const CustomIcon = () => (
<svg viewBox="0 0 24 24" fill="red" width={24} height={24}>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
</svg>
);
@@ -0,0 +1,19 @@
/*
* 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 * from './Icon';
export { IconProvider } from './context';
export type { IconNames } from './context';
@@ -0,0 +1,58 @@
/*
* 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 { Inline } from './Inline';
import { Box } from '../Box/Box';
const meta = {
title: 'Components/Inline',
component: Inline,
} satisfies Meta<typeof Inline>;
export default meta;
type Story = StoryObj<typeof meta>;
const FakeBox = () => (
<Box
paddingX="xl"
paddingY="md"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
Fake Box
</Box>
);
export const Default: Story = {
args: {
children: (
<>
<FakeBox />
<FakeBox />
<FakeBox />
</>
),
},
};
export const LargeGap: Story = {
args: {
...Default.args,
gap: 'xl',
},
};
@@ -0,0 +1,58 @@
/*
* 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 { Box, BoxProps } from '../Box/Box';
import { alignToFlexAlign } from '../../utils/align';
export const validInlineComponents = [
'div',
'span',
'p',
'nav',
'ul',
'ol',
'li',
] as const;
export interface InlineProps extends Omit<BoxProps, 'alignItems'> {
as?: (typeof validInlineComponents)[number];
children: React.ReactNode;
align?: 'left' | 'center' | 'right';
gap?: BoxProps['gap'];
}
export const Inline = ({
align,
as = 'div',
children,
gap = 'xs',
...restProps
}: InlineProps) => {
return (
<Box
as={as}
display="flex"
alignItems={align !== 'left' ? alignToFlexAlign(align) : undefined}
justifyContent="flex-start"
flexWrap="wrap"
gap={gap}
{...restProps}
>
{children}
</Box>
);
};
@@ -0,0 +1,16 @@
/*
* 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 { Inline } from './Inline';
@@ -0,0 +1,58 @@
/*
* 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 { Stack } from './Stack';
import { Box } from '../Box/Box';
const meta = {
title: 'Components/Stack',
component: Stack,
} satisfies Meta<typeof Stack>;
export default meta;
type Story = StoryObj<typeof meta>;
const FakeBox = () => (
<Box
paddingX="xl"
paddingY="md"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
Fake Box
</Box>
);
export const Default: Story = {
args: {
children: (
<>
<FakeBox />
<FakeBox />
<FakeBox />
</>
),
},
};
export const LargeGap: Story = {
args: {
...Default.args,
gap: 'xl',
},
};
@@ -0,0 +1,74 @@
/*
* 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 { Box, BoxProps } from '../Box/Box';
import { alignToFlexAlign } from '../../utils/align';
const validStackComponents = [
'div',
'span',
'p',
'article',
'section',
'main',
'nav',
'aside',
'ul',
'ol',
'li',
'details',
'summary',
'dd',
'dl',
'dt',
] as const;
export interface StackProps extends Omit<BoxProps, 'alignItems'> {
children: React.ReactNode;
as?: (typeof validStackComponents)[number];
align?: 'left' | 'center' | 'right';
gap?: BoxProps['gap'];
}
export const Stack = ({
as = 'div',
children,
align: alignProp,
gap = 'xs',
...restProps
}: StackProps) => {
/**
* Creating a seam between the provided prop and the default value
* to enable only setting the text alignment when the `align` prop
* is provided — not when it's defaulted.
*/
const align = alignProp || 'left';
return (
<Box
as={as}
display="flex"
flexDirection="column"
alignItems={align !== 'left' ? alignToFlexAlign(align) : undefined}
gap={gap}
// textAlign={alignProp}
{...restProps}
>
{children}
</Box>
);
};
@@ -0,0 +1,16 @@
/*
* 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 { Stack } from './Stack';
@@ -0,0 +1,86 @@
/*
* 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 { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
import { breakpoints, space } from '../Box/properties';
import { colorProperties } from '../Box/sprinkles.css';
const responsiveProperties = defineProperties({
conditions: breakpoints,
defaultCondition: 'xs',
properties: {
flexDirection: ['row', 'column'],
justifyContent: [
'stretch',
'flex-start',
'center',
'flex-end',
'space-around',
'space-between',
],
alignItems: ['stretch', 'flex-start', 'center', 'flex-end'],
borderRadius: {
none: 0,
small: '4px',
medium: '8px',
full: '9999px',
},
boxShadow: {
small: 'var(--canon-box-shadow-small)',
medium: 'var(--canon-box-shadow-medium)',
large: 'var(--canon-box-shadow-large)',
},
border: {
none: 'none',
thin: '1px solid var(--canon-outline)',
error: '1px solid var(--canon-error)',
},
display: ['none', 'flex', 'block', 'inline'],
paddingTop: space,
paddingBottom: space,
paddingLeft: space,
paddingRight: space,
marginTop: space,
marginBottom: space,
marginLeft: space,
marginRight: space,
gap: space,
flexWrap: ['wrap', 'nowrap'],
},
shorthands: {
p: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
pt: ['paddingTop'],
pr: ['paddingRight'],
pb: ['paddingBottom'],
pl: ['paddingLeft'],
px: ['paddingLeft', 'paddingRight'],
py: ['paddingTop', 'paddingBottom'],
m: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
mt: ['marginTop'],
mr: ['marginRight'],
mb: ['marginBottom'],
ml: ['marginLeft'],
mx: ['marginLeft', 'marginRight'],
my: ['marginTop', 'marginBottom'],
direction: ['flexDirection'],
items: ['alignItems'],
justify: ['justifyContent'],
},
});
export const sprinkles = createSprinkles(responsiveProperties, colorProperties);
// It's a good idea to export the Sprinkles type too
export type Sprinkles = Parameters<typeof sprinkles>[0];
+25
View File
@@ -0,0 +1,25 @@
/*
* 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.
*/
/**
* Components used by Backstage plugins and apps
*
* @packageDocumentation
*/
export * from './components/Button';
export * from './components/Box';
export * from './components/Icon';
+25
View File
@@ -0,0 +1,25 @@
/*
* 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.
*/
/* Geist font */
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&display=swap');
/* Theme */
@import './theme.css';
/* Components */
@import '../components/Button/styles.css';
+57
View File
@@ -0,0 +1,57 @@
/*
* 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.
*/
/* Light theme */
:root {
--canon-accent: #1ed760;
--canon-background: #f8f8f8;
--canon-surface-1: #fff;
--canon-surface-2: #f4f4f4;
--canon-outline: #666;
--canon-outline-focus: #ccc;
--canon-error: #f50000;
--canon-text-primary: #000;
--canon-text-secondary: #646464;
--canon-box-shadow-small: 0 0 0 1px rgba(0, 0, 0, 0.05);
--canon-box-shadow-medium: 0 0 0 1px rgba(0, 0, 0, 0.1);
--canon-box-shadow-large: 0 0 0 1px rgba(0, 0, 0, 0.2);
--canon-font-sans: 'Geist', serif;
--canon-font-monospace: 'Monospace', monospace;
--canon-font-emoji: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--canon-space-unit: 1em;
--canon-space-xxs: calc(0.25 * var(--canon-space-unit));
--canon-space-xs: calc(0.5 * var(--canon-space-unit));
--canon-space-sm: calc(0.75 * var(--canon-space-unit));
--canon-space-md: calc(1.25 * var(--canon-space-unit));
--canon-space-lg: calc(2 * var(--canon-space-unit));
--canon-space-xl: calc(3.25 * var(--canon-space-unit));
--canon-space-xxl: calc(5.25 * var(--canon-space-unit));
}
/* Dark theme */
[data-theme='dark'] {
--canon-accent: #1ed760;
--canon-background: #000;
--canon-surface-1: #121212;
--canon-surface-2: #292929;
--canon-outline: #666;
--canon-outline-focus: #ccc;
--canon-text-primary: #f0f0f0;
--canon-text-secondary: #666;
}
+49
View File
@@ -0,0 +1,49 @@
/*
* 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 OptionalResponsiveValue,
mapResponsiveValue,
} from '../components/Box/sprinkles.css';
export type Align = 'left' | 'center' | 'right';
export type AlignY = 'top' | 'center' | 'bottom';
const alignToFlexAlignLookup = {
left: 'flex-start',
center: 'center',
right: 'flex-end',
} as const;
export const alignToFlexAlign = (
align: OptionalResponsiveValue<Align> | undefined,
) =>
align
? mapResponsiveValue(align, value => alignToFlexAlignLookup[value])
: undefined;
const alignYToFlexAlignLookup = {
top: 'flex-start',
center: 'center',
bottom: 'flex-end',
} as const;
export const alignYToFlexAlign = (
alignY: OptionalResponsiveValue<AlignY> | undefined,
) =>
alignY
? mapResponsiveValue(alignY, value => alignYToFlexAlignLookup[value])
: undefined;
+42
View File
@@ -0,0 +1,42 @@
/*
* 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 {
responsiveProperties,
colorProperties,
} from '../components/Box/sprinkles.css';
export const listResponsiveValues = (
value: keyof typeof responsiveProperties.styles,
) => {
const values = responsiveProperties.styles[value];
if ('values' in values) {
return Object.keys(values.values);
}
return [];
};
export const listColorValues = (value: keyof typeof colorProperties.styles) => {
const values = colorProperties.styles[value];
if ('values' in values) {
return Object.keys(values.values);
}
return [];
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+1862 -265
View File
File diff suppressed because it is too large Load Diff