Merge pull request #28317 from backstage/canon-website

Canon website
This commit is contained in:
Charles de Dreuille
2025-01-10 12:02:20 +00:00
committed by GitHub
106 changed files with 17055 additions and 10 deletions
+1
View File
@@ -7,6 +7,7 @@
**/.git/**
**/public/**
**/microsite/**
**/canon-docs/**
**/templates/**
**/sample-templates/**
playwright.config.ts
+11 -10
View File
@@ -1,4 +1,4 @@
name: Sync Canon Storybook
name: Sync Canon Docs
on:
push:
branches: [master]
@@ -30,27 +30,28 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: backstage/canon-storybook
path: canon-storybook
path: canon-external-docs
token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
- name: Configure Git
run: |
git config --global user.email noreply@backstage.io
git config --global user.name 'Github Canon Storybook workflow'
git config --global user.name 'Github Canon Docs workflow'
- name: Build Canon Storybook
- name: Build Canon Docs
working-directory: canon-docs
run: |
yarn workspace @backstage/canon run build-storybook
yarn build
- name: Replace contents of canon-storybook repo with Storybook build output
working-directory: canon-storybook
- name: Replace contents of canon-external-docs repo with docs build output
working-directory: canon-external-docs
run: |
git rm -rf .
cp -R ../packages/canon/storybook-static/. .
cp -R ../canon-docs/dist/. .
- name: Commit to canon-storybook repo
working-directory: canon-storybook
working-directory: canon-external-docs
run: |
git add .
git commit -am "Canon Storybook build for backstage/backstage@${{ github.sha }}"
git commit -am "Canon Docs build for backstage/backstage@${{ github.sha }}"
git push
+3
View File
@@ -1,6 +1,9 @@
.yarn
dist
microsite
canon-docs/.next
canon-docs/public
canon-docs/out
coverage
*.hbs
templates
+8
View File
@@ -0,0 +1,8 @@
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"notice/notice": "off",
"react/forbid-elements": "off",
"jsx-a11y/alt-text": "off"
}
}
+4
View File
@@ -0,0 +1,4 @@
# next.js
/.next/
/dist/
next-env.d.ts
+15
View File
@@ -0,0 +1,15 @@
# Canon Docs
Canon is our internal UI library built for Backstage. We built this website to document the library and its components. You can view this website [here](https://canon.backstage.io).
## How to run locally
This website is built with Next.js and it is hosted on Github pages. To run it locally, you can run the following command:
```bash
yarn start
```
## Deployment
Deployments are done automatically when a PR is merged into the `master` branch. We host the website using Github pages.
+15
View File
@@ -0,0 +1,15 @@
import createMDX from '@next/mdx';
const nextConfig = {
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
output: 'export',
assetPrefix: '/',
distDir: 'dist',
images: {
unoptimized: true,
},
};
const withMDX = createMDX({});
export default withMDX(nextConfig);
+34
View File
@@ -0,0 +1,34 @@
{
"name": "canon-docs",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"lint": "next lint",
"start": "concurrently \"next dev\" \"yarn watch-css\"",
"watch-css": "node scripts/watch-css.js"
},
"dependencies": {
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "^15.1.4",
"@storybook/react": "^8.4.7",
"@types/mdx": "^2.0.13",
"next": "14.2.23",
"react": "^18",
"react-dom": "^18",
"react-frame-component": "^5.2.7",
"shiki": "^1.26.1",
"storybook": "^8.4.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"concurrently": "^9.1.2",
"eslint": "^8",
"eslint-config-next": "14.2.23",
"lightningcss": "^1.28.2",
"typescript": "^5"
}
}
View File
+1
View File
@@ -0,0 +1 @@
canon.backstage.io
+71
View File
@@ -0,0 +1,71 @@
[data-theme-name="legacy"][data-theme="light"], [data-theme-name="legacy"][data-theme="dark"] {
--canon-font-regular: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
& .cn-button {
text-transform: uppercase;
box-shadow: none;
background-color: #1f5493;
border-radius: 4px;
padding: 6px 16px;
font-size: .875rem;
font-weight: 400;
line-height: 1.75;
transition: background-color .25s cubic-bezier(.4, 0, .2, 1), box-shadow .25s cubic-bezier(.4, 0, .2, 1), border .25s cubic-bezier(.4, 0, .2, 1);
&:hover {
box-shadow: none;
}
&.cn-button-primary {
color: #fff;
background-color: #153a66;
box-shadow: 0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f;
&:hover {
box-shadow: 0 2px 4px -1px #0003, 0 4px 5px #00000024, 0 1px 10px #0000001f;
}
}
&.cn-button-secondary {
color: #1f5493;
background-color: #0000;
border: 1px solid #1f549380;
&:hover {
background-color: #1f54930a;
border: 1px solid #1f5493;
}
}
&.cn-button-tertiary {
color: #1f5493;
background-color: #0000;
border: none;
}
}
}
[data-theme-name="legacy"][data-theme="light"] {
--canon-accent: #2e77d0;
--canon-background: #f8f8f8;
--canon-surface-1: #fff;
--canon-surface-2: #f4f4f4;
--canon-text-primary: #000;
--canon-text-primary-on-accent: #fff;
--canon-text-secondary: #646464;
}
[data-theme-name="legacy"][data-theme="dark"] {
--canon-accent: #fff;
--canon-background: #000;
--canon-surface-1: #121212;
--canon-surface-2: #1a1a1a;
--canon-border-base: #fff3;
--canon-border-warning: #f50000;
--canon-border-error: #f87503;
--canon-border-selected: #25d262;
--canon-error: #f50000;
--canon-text-primary: #fff;
--canon-text-primary-on-accent: #000;
--canon-text-secondary: #b3b3b3;
}
+279
View File
@@ -0,0 +1,279 @@
.cn-button {
all: unset;
user-select: none;
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-bold);
font-size: var(--canon-font-size-body);
cursor: pointer;
border-radius: 8px;
justify-content: center;
align-items: center;
padding: 0;
transition: all .15s;
display: inline-flex;
}
.cn-button-primary {
background-color: var(--canon-accent);
color: var(--canon-text-primary-on-accent);
&:hover {
box-shadow: inset 0 0 0 1px var(--canon-border-focus);
color: var(--canon-text-primary);
background-color: #0000;
}
}
.cn-button-secondary {
box-shadow: inset 0 0 0 1px var(--canon-border-base);
color: var(--canon-text-primary);
background-color: #0000;
&:hover {
box-shadow: inset 0 0 0 1px var(--canon-border-hover);
}
}
.cn-button-tertiary {
color: var(--canon-text-primary);
background-color: #0000;
&:hover {
color: var(--canon-text-secondary);
}
}
.cn-button-small {
height: 32px;
padding-left: 6px;
padding-right: 6px;
}
.cn-button-medium {
height: 40px;
padding-left: 8px;
padding-right: 8px;
}
.cn-button-content {
align-items: center;
gap: var(--canon-spacing-xs);
font-weight: var(--canon-font-weight-bold);
display: flex;
}
.cn-button-content-icon-both {
flex: 1;
}
.canon-stack {
flex-direction: column;
display: flex;
}
.canon-inline {
flex-wrap: wrap;
display: flex;
}
.canon-grid {
display: grid;
}
.canon-container {
max-width: var(--canon-container-max-width);
padding: 0 var(--canon-container-padding);
margin: 0 auto;
}
.icon-12 {
width: 12px;
height: 12px;
}
.icon-16 {
width: 16px;
height: 16px;
}
.icon-24 {
width: 24px;
height: 24px;
}
.checkbox-label {
justify-content: center;
align-items: center;
gap: var(--canon-spacing-xs);
font-size: var(--canon-font-size-xs);
font-family: var(--canon-font-regular);
color: var(--canon-text-primary);
user-select: none;
flex-direction: row;
display: flex;
&:hover {
& .checkbox {
box-shadow: inset 0 0 0 1px var(--canon-border-hover);
}
}
}
.checkbox {
all: unset;
width: 1rem;
height: 1rem;
box-shadow: inset 0 0 0 1px var(--canon-border-base);
cursor: pointer;
border-radius: 2px;
justify-content: center;
align-items: center;
transition: all .2s ease-in-out;
display: flex;
&[data-checked] {
background-color: var(--canon-accent);
}
}
.checkbox-indicator {
color: var(--canon-text-primary-on-accent);
justify-content: center;
align-items: center;
display: flex;
}
.table {
background-color: var(--canon-surface-1);
border-radius: var(--canon-border-radius-xs);
width: 100%;
padding-bottom: var(--canon-spacing-5xs);
padding-top: var(--canon-spacing-5xs);
font-size: var(--canon-font-size-body);
position: relative;
overflow: auto;
& table {
caption-side: bottom;
width: 100%;
font-size: var(--canon-font-size-sm);
border-collapse: collapse;
}
}
.table-head {
text-align: left;
padding: var(--canon-spacing-xs);
}
.table-body {
& tr:last-child {
border-bottom: none;
}
}
.table-row {
transition: color .2s ease-in-out;
&:hover td {
background-color: var(--canon-surface-3);
}
& .table-cell:first-child {
border-top-left-radius: var(--canon-border-radius-xs);
border-bottom-left-radius: var(--canon-border-radius-xs);
box-shadow: inset 4px 2px 0 0 var(--canon-surface-1), inset 4px -2px 0 0 var(--canon-surface-1);
padding-left: var(--canon-spacing-xs);
}
& .table-cell:last-child {
border-top-right-radius: var(--canon-border-radius-xs);
border-bottom-right-radius: var(--canon-border-radius-xs);
box-shadow: inset -4px 2px 0 0 var(--canon-surface-1), inset -4px -2px 0 0 var(--canon-surface-1);
padding-right: var(--canon-spacing-xs);
}
}
.table-cell {
padding: var(--canon-spacing-xs);
background-color: var(--canon-surface-2);
box-shadow: inset 0px 2px 0 0 var(--canon-surface-1), inset 0px -2px 0 0 var(--canon-surface-1);
}
.text {
font-family: var(--canon-font-regular);
color: var(--canon-text-primary);
margin: 0;
padding: 0;
&.text-body {
font-size: var(--canon-font-size-body);
line-height: 140%;
}
&.text-subtitle {
font-size: var(--canon-font-size-subtitle);
line-height: 140%;
}
&.text-caption {
font-size: var(--canon-font-size-caption);
line-height: 140%;
}
&.text-label {
font-size: var(--canon-font-size-label);
line-height: 140%;
}
&.text-regular {
font-weight: var(--canon-font-weight-regular);
}
&.text-bold {
font-weight: var(--canon-font-weight-bold);
}
}
.text {
font-family: var(--canon-font-regular);
color: var(--canon-text-primary);
margin: 0;
padding: 0;
&.text-display {
font-size: var(--canon-font-size-display);
line-height: 100%;
font-weight: var(--canon-font-weight-bold);
}
&.text-title1 {
font-size: var(--canon-font-size-title1);
line-height: 100%;
font-weight: var(--canon-font-weight-bold);
}
&.text-title2 {
font-size: var(--canon-font-size-title2);
line-height: 100%;
font-weight: var(--canon-font-weight-bold);
}
&.text-title3 {
font-size: var(--canon-font-size-title3);
line-height: 100%;
font-weight: var(--canon-font-weight-bold);
}
&.text-title4 {
font-size: var(--canon-font-size-title4);
line-height: 100%;
font-weight: var(--canon-font-weight-bold);
}
&.text-title5 {
font-size: var(--canon-font-size-title5);
line-height: 100%;
font-weight: var(--canon-font-weight-bold);
}
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

+55
View File
@@ -0,0 +1,55 @@
const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
const { bundle } = require('lightningcss');
const source = '../../packages/canon/src/css';
const destination = '../public';
const source1 = path.join(__dirname, `${source}/core.css`);
const destination1 = path.join(__dirname, `${destination}/core.css`);
const source2 = path.join(__dirname, `${source}/components.css`);
const destination2 = path.join(__dirname, `${destination}/components.css`);
const source3 = path.join(
__dirname,
`../../packages/canon/.storybook/themes/backstage.css`,
);
const destination3 = path.join(__dirname, `${destination}/backstage.css`);
// Function to bundle and copy the CSS file
const bundleAndCopyFile = async (source, destination) => {
try {
const result = await bundle({
filename: source,
minify: false,
});
fs.writeFileSync(destination, result.code);
console.log('File bundled and copied successfully!');
} catch (err) {
console.error('Error bundling file:', err);
}
};
// Watch the source file for changes
chokidar.watch(source1).on('change', () => {
console.log('Detected change in core.css, bundling and copying...');
bundleAndCopyFile(source1, destination1);
});
// Watch the components.css file for changes
chokidar.watch(source2).on('change', () => {
console.log('Detected change in components.css, bundling and copying...');
bundleAndCopyFile(source2, destination2);
});
// Watch the backstage.css file for changes
chokidar.watch(source3).on('change', () => {
console.log('Detected change in backstage.css, bundling and copying...');
bundleAndCopyFile(source3, destination3);
});
// Initial bundle and copy
bundleAndCopyFile(source1, destination1);
bundleAndCopyFile(source2, destination2);
bundleAndCopyFile(source3, destination3);
+32
View File
@@ -0,0 +1,32 @@
# About Canon
Canon is a design system created specifically for Backstage, built with React, TypeScript, and vanilla CSS.
This open-source library is hosted in the Backstage monorepo. While it can be used in other projects, Canon
is designed to deliver a consistent, accessible, and extensible experience tailored to Backstage users.
## Philosophy
Backstage empowers product teams to build software faster and with greater quality. Its extensibility,
however, required us to rethink how to deliver a consistent and accessible user experience. Our goal is
to enable plugin creators to design plugins that seamlessly integrate with Backstage's look and feel while
still allowing customization to match individual brands.
Instead of reinventing the wheel, we chose to focus on layout and styling while leveraging existing headless
component libraries for functionality. This approach allows us to dedicate our efforts to creating a cohesive
and flexible theming system.
## Team
Canon is designed and maintained primarily by Spotify's Backstage team, leveraging Spotify's expertise in
crafting high-quality design and technology. Drawing from our experience in building reliable and intuitive
user experiences for the music industry, we've created a design system that looks great and works seamlessly.
## Community
Canon is an open-source project and we welcome contributions from the community. If you are interested in
contributing to Canon, please read our [contributing guide](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)
and our [code of conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md).
## License
Canon is licensed under the Apache 2.0 license. See the [LICENSE](https://github.com/backstage/backstage/blob/master/LICENSE) file for more details.
@@ -0,0 +1,112 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { spacePropsList } from '@/utils/spaceProps';
import { Tabs } from '@/components/Tabs';
import { Snippet } from '@/components/Snippet';
import { BoxPreview } from '@/snippets/box';
# Box
Box is the lowest-level component in Canon. It provides a consistent API for styling and layout.
<Snippet
py={4}
preview={<BoxPreview />}
code={`<Box>
<DecorativeBox />
</Box>`}
align="center"
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock code={`import { Box } from "@backstage/canon";
<Box />`} />
</Tabs.Panel>
</Tabs.Root>
## API reference
### Box
This is the Box component, our lowest-level component. Here are all the
available properties.
<PropsTable
data={{
alignItems: {
type: ['stretch', 'start', 'center', 'end'],
responsive: true,
},
border: {
type: ['none', 'base', 'error', 'warning', 'selected'],
responsive: true,
},
borderRadius: {
type: ['none', '2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'],
responsive: true,
},
children: {
type: 'ReactNode',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
display: {
type: ['none', 'flex', 'block', 'inline'],
responsive: true,
},
flexDirection: {
type: ['row', 'column'],
responsive: true,
},
flexWrap: {
type: ['wrap', 'nowrap', 'wrap-reverse'],
responsive: true,
},
gap: {
type: ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl'],
responsive: true,
},
justifyContent: {
type: ['stretch', 'start', 'center', 'end', 'around', 'between'],
responsive: true,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
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.
<PropsTable data={spacePropsList} />
## Examples
### Simple example
A simple example of how to use the Box component.
<CodeBlock code={`<Box padding="md" borderRadius="md">Hello World</Box>`} />
### Responsive
Here's a view when buttons are responsive.
<CodeBlock
code={`<Box
padding={{ xs: 'sm', md: 'md' }}
borderRadius={{ xs: 'sm', md: 'md' }}>
Hello World
</Box>`}
/>
@@ -0,0 +1,175 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import {
ButtonPreview,
ButtonSizes,
ButtonWithIcons,
ButtonFullWidth,
ButtonDisabled,
ButtonResponsive,
} from '@/snippets/button';
import { buttonVariants } from '@/snippets/_snippets';
# Button
A button component that can be used to trigger actions.
<Snippet
align="center"
py={4}
preview={<ButtonPreview />}
code={`<Inline alignY="center">
<Button iconStart="cloud" variant="primary">
Button
</Button>
<Button iconStart="cloud" variant="secondary">
Button
</Button>
<Button iconStart="cloud" variant="tertiary">
Button
</Button>
</Inline>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Button } from '@backstage/canon';
<Button />
`}
/>
</Tabs.Panel>
<Tabs.Panel>
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
your brand. For additional flexibility, you can use the provided class names for each element listed below.
<CodeBlock
code={`<Button className="button" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
size: {
type: ['small', 'medium'],
responsive: true,
},
variant: {
type: ['primary', 'secondary', 'tertiary'],
responsive: true,
},
disabled: {
type: 'boolean',
responsive: false,
},
children: {
type: 'ReactNode',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<ButtonPreview />}
code={buttonVariants}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<ButtonSizes />}
code={`<Inline alignY="center">
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
</Inline>`}
/>
### With Icons
Here's a view when buttons have icons.
<Snippet
align="center"
py={4}
open
preview={<ButtonWithIcons />}
code={`<Inline alignY="center">
<Button iconStart="cloud">Button</Button>
<Button iconEnd="chevronRight">Button</Button>
<Button iconStart="cloud" iconEnd="chevronRight">Button</Button>
</Inline>`}
/>
### Full width
Here's a view when buttons are full width.
<Snippet
align="center"
py={4}
open
preview={<ButtonFullWidth />}
code={`<Stack style={{ width: '300px' }}>
<Button fullWidth>Full width</Button>
</Stack>`}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<ButtonDisabled />}
code={`<Button disabled>Button</Button>`}
/>
### Responsive
Here's a view when buttons are responsive.
<Snippet
align="center"
py={4}
open
preview={<ButtonResponsive />}
code={`<Button
variant={{ xs: 'primary', sm: 'secondary', md: 'tertiary' }}
size={{ xs: 'small', sm: 'medium' }}>
Button
</Button>`}
/>
@@ -0,0 +1,107 @@
import { PropsTable } from '@/components/PropsTable';
import { CheckboxPreview, CheckboxAllVariants } from '@/snippets/checkbox';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import { BaseUI } from '@/components/HeadlessBanners/BaseUI';
# Checkbox
A checkbox component that can be used to trigger actions.
<Snippet
py={4}
preview={<CheckboxPreview />}
code={`<Checkbox label="Accept terms and conditions" />`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Checkbox } from '@backstage/canon';
<Checkbox />
`}
/>
</Tabs.Panel>
<Tabs.Panel>
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
your brand. For additional flexibility, you can use the provided class names for each element listed below.
<CodeBlock
code={`<Checkbox className="checkbox" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<BaseUI href="https://base-ui.com/react/components/checkbox" />
<PropsTable
data={{
label: {
type: 'string',
responsive: false,
},
defaultChecked: {
type: ['boolean', "'indeterminate'"],
responsive: false,
},
checked: {
type: ['boolean', "'indeterminate'"],
responsive: false,
},
onChange: {
type: "(checked: boolean | 'indeterminate') => void",
responsive: false,
},
disabled: {
type: 'boolean',
responsive: false,
},
required: {
type: 'boolean',
responsive: false,
},
name: {
type: 'string',
responsive: false,
},
value: {
type: 'string',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
## Examples
### All variants
Here's a view when checkboxes have different variants.
<Snippet
align="center"
py={4}
open
preview={<CheckboxAllVariants />}
code={`<Inline alignY="center">
<Checkbox />
<Checkbox checked />
<Checkbox label="Checkbox" />
<Checkbox label="Checkbox" checked />
</Inline>`}
/>
@@ -0,0 +1,101 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { spacePropsList } from '@/utils/spaceProps';
import { Tabs } from '@/components/Tabs';
import { Snippet } from '@/components/Snippet';
import { ContainerPreview } from '@/snippets/container';
# Container
The container component let you use our default max-width and center the
content on the page.
<Snippet
py={4}
preview={<ContainerPreview />}
code={`<Container>
<DecorativeBox />
</Container>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock code={`import { Container } from "@backstage/canon";
<Container>Hello World!</Container>
`} />
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
children: {
type: 'ReactNode',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
marginY: {
type: spacePropsList.marginY.type,
responsive: spacePropsList.marginY.responsive,
},
marginBottom: {
type: spacePropsList.marginBottom.type,
responsive: spacePropsList.marginBottom.responsive,
},
marginTop: {
type: spacePropsList.marginTop.type,
responsive: spacePropsList.marginTop.responsive,
},
paddingY: {
type: spacePropsList.paddingY.type,
responsive: spacePropsList.paddingY.responsive,
},
paddingBottom: {
type: spacePropsList.paddingBottom.type,
responsive: spacePropsList.paddingBottom.responsive,
},
paddingTop: {
type: spacePropsList.paddingTop.type,
responsive: spacePropsList.paddingTop.responsive,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
## Examples
### Simple
A simple example of how to use the Container component.
<CodeBlock
code={`<Container>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Container>`}
/>
### Responsive padding & margin
The Container component also supports responsive values, making it easy to
create responsive designs.
<CodeBlock
code={`<Container paddingY={{ xs: 'sm', md: 'md' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Container>`}
/>
@@ -0,0 +1,191 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { spacePropsList } from '@/utils/spaceProps';
import { Tabs } from '@/components/Tabs';
import { grid } from '@/snippets/_snippets';
import { Snippet } from '@/components/Snippet';
import { GridPreview } from '@/snippets/grid';
# Grid
A layout component that helps to create simple column-based layouts as well as
more complex ones.
<Snippet
py={4}
preview={<GridPreview />}
code={`<Grid>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Grid>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock code={grid} />
</Tabs.Panel>
</Tabs.Root>
## API reference
### Grid
This is the grid container component. It will help to define the number of
columns that will be used in the grid. You can also define the gap between the
columns. All values are responsive.
<PropsTable
data={{
columns: {
type: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'auto'],
responsive: true,
},
gap: {
type: ['xs', 'sm', 'md', 'lg', 'xl'],
responsive: true,
},
children: {
type: 'ReactNode',
required: false,
},
className: {
type: 'string',
required: false,
},
style: {
type: 'CSSProperties',
required: false,
},
}}
/>
The grid component also accepts all the spacing props from the Box component.
<PropsTable data={spacePropsList} />
### Grid.Item
If you need more control over the columns, you can use the grid item
component. This will give you access to `rowSpan`, `colSpan`, `start` and
`end`. All values are responsive. This component is optional, you can use any
elements directly if you prefer.
<PropsTable
data={{
colSpan: {
type: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'full'],
responsive: true,
},
rowSpan: {
type: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'full'],
responsive: true,
},
start: {
type: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'auto'],
responsive: true,
},
end: {
type: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'auto'],
responsive: true,
},
children: {
type: 'ReactNode',
required: false,
},
className: {
type: 'string',
required: false,
},
style: {
type: 'CSSProperties',
required: false,
},
}}
/>
## Examples
### Simple grid
A simple grid with 3 columns and a gap of md.
<CodeBlock
code={`<Grid columns={3} gap="md">
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Grid>
`}
/>
### Complex grid
You can also use the grid item to create more complex layouts. In this example
the first column will span 1 column and the second column will span 2 columns.
<CodeBlock
code={`<Grid columns={3} gap="md">
<Grid.Item colSpan={1}>
<Box>Hello World</Box>
</Grid.Item>
<Grid.Item colSpan={2}>
<Box>Hello World</Box>
</Grid.Item>
</Grid>
`}
/>
### Mixing rows and columns
The grid item component also supports the `rowSpan` prop, which allows you to
span multiple rows within the grid layout. In this example, the first item
will span 2 rows to achieve a dynamic and flexible grid structure.
<CodeBlock
code={`<Grid columns={3} gap="md">
<Grid.Item colSpan={1} rowSpan={2}>
<Box>Hello World</Box>
</Grid.Item>
<Grid.Item colSpan={2}>
<Box>Hello World</Box>
</Grid.Item>
<Grid.Item colSpan={2}>
<Box>Hello World</Box>
</Grid.Item>
</Grid>
`}
/>
### Responsive grid
The grid component also supports responsive values, making it easy to create
responsive designs.
<CodeBlock
code={`<Grid columns={{ xs: 1, md: 3 }} gap={{ xs: 'xs', md: 'md' }}>
<Grid.Item colSpan={{ xs: 1, md: 2 }}>
<Box>Hello World</Box>
</Grid.Item>
<Grid.Item colSpan={{ xs: 1, md: 1 }}>
<Box>Hello World</Box>
</Grid.Item>
</Grid>
`}
/>
### Start and End
The start and end props can be used to position the item in the grid.
<CodeBlock
code={`<Grid columns={3} gap="md">
<Grid.Item start={2} end={4}>
<Box>Hello World</Box>
</Grid.Item>
</Grid>
`}
/>
@@ -0,0 +1,97 @@
import { PropsTable } from '@/components/PropsTable';
import {
HeadingPreview,
HeadingAllVariants,
HeadingResponsive,
} from '@/snippets/heading';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
# Heading
Headings are used to structure the content of your page.
<Snippet
py={4}
preview={<HeadingPreview />}
code={`<Heading variant="title1">Hello World!</Heading>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Heading } from '@backstage/canon';
<Heading />
`}
/>
</Tabs.Panel>
<Tabs.Panel>
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
your brand. For additional flexibility, you can use the provided class names for each element listed below.
<CodeBlock
code={`<Heading className="heading" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
variant: {
type: ['display', 'title1', 'title2', 'title3', 'title4', 'title5'],
responsive: true,
},
children: {
type: 'ReactNode',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
## Examples
### All variants
The `Heading` component has a `variant` prop that can be used to change the
appearance of the heading.
<Snippet
py={2}
open
preview={<HeadingAllVariants />}
code={`<Stack gap="md">
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Stack>`}
/>
### Responsive
You can also use the `variant` prop to change the appearance of the text based
on the screen size.
<Snippet
py={4}
open
preview={<HeadingResponsive />}
code={`<Heading variant={{ xs: 'title1', md: 'title2' }}>Responsive</Heading>`}
/>
@@ -0,0 +1,63 @@
import { PropsTable } from '@/components/PropsTable';
import { icons } from '@backstage/canon';
import { IconPreview } from '@/snippets/icon';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
# Icon
Icons are used to represent an action or a state.
<Snippet
py={4}
align="center"
preview={<IconPreview />}
code={`<Icon name="heart" />`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Icon } from '@backstage/canon';
<Icon />
`}
/>
</Tabs.Panel>
<Tabs.Panel>
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
your brand. For additional flexibility, you can use the provided class names for each element listed below.
<CodeBlock
code={`<Icon className="icon" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
name: {
type: Object.keys(icons),
responsive: false,
},
size: {
type: 'number',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
@@ -0,0 +1,118 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { spacePropsList } from '@/utils/spaceProps';
import { Tabs } from '@/components/Tabs';
import { Snippet } from '@/components/Snippet';
import { InlinePreview } from '@/snippets/inline';
# Inline
The Inline component is used to create a horizontal layout of elements.
<Snippet
py={4}
preview={<InlinePreview />}
code={`<Inline>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Inline>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Inline } from '@backstage/canon';
<Inline />
`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
align: {
type: 'start | center | end',
responsive: true,
},
alignY: {
type: 'start | center | end',
responsive: true,
},
children: {
type: 'ReactNode',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
The grid component also accepts all the spacing props from the Box component.
<PropsTable data={spacePropsList} />
## Examples
### Simple
A simple example of how to use the Inline component.
<CodeBlock
code={`<Inline>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Inline>`}
/>
### Responsive
The Inline component also supports responsive values, making it easy to create
responsive designs.
<CodeBlock
code={`<Inline gap={{ xs: 'sm', md: 'md' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Inline>`}
/>
### Align
The Inline component also supports responsive alignment, making it easy to
create responsive designs.
<CodeBlock
code={`<Inline align={{ xs: 'left', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Inline>`}
/>
### Align vertically
The Inline component also supports responsive vertical alignment, making it
easy to create responsive designs.
<CodeBlock
code={`<Inline alignY={{ xs: 'top', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Inline>`}
/>
@@ -0,0 +1,101 @@
import { PropsTable } from '@/components/PropsTable';
import { spacePropsList } from '@/utils/spaceProps';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import {
stackFAQ1,
stackSimple,
stackResponsive,
stackAlign,
} from '@/snippets/_snippets';
import { Snippet } from '@/components/Snippet';
import { StackPreview } from '@/snippets/stack';
# Stack
This is the stack container component. It will help to define the number of
columns that will be used in the grid. You can also define the gap between the
columns. All values are responsive.
<Snippet
py={4}
align="center"
preview={<StackPreview />}
code={`<Stack>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Stack>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Stack } from '@backstage/canon';
<Stack />
`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
align: {
type: ['start', 'center', 'end'],
responsive: true,
},
children: {
type: 'ReactNode',
required: false,
},
className: {
type: 'string',
required: false,
},
style: {
type: 'CSSProperties',
required: false,
},
}}
/>
The grid component also accepts all the spacing props from the Box component.
<PropsTable data={spacePropsList} />
## Common questions
### Can I stack horizontally?
The Stack component only allows for stacking elements vertically. If you want
to create a column layout, please use the Grid component.
<CodeBlock code={stackFAQ1} />
## Examples
### Simple
A simple example of how to use the Stack component.
<CodeBlock code={stackSimple} />
### Responsive
The Stack component also supports responsive values, making it easy to create
responsive designs.
<CodeBlock code={stackResponsive} />
### Align
The Stack component also supports responsive alignment, making it easy to
create responsive designs.
<CodeBlock code={stackAlign} />
@@ -0,0 +1 @@
# Table
@@ -0,0 +1,141 @@
import { PropsTable } from '@/components/PropsTable';
import {
TextPreview,
TextAllVariants,
TextResponsive,
TextAllWeights,
} from '@/snippets/text';
import { Snippet } from '@/components/Snippet';
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
# Text
The `Text` component is used to display content on your page.
<Snippet
py={4}
preview={<TextPreview />}
code={`<Text style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is in
the painting?
</Text>`}
/>
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
<Tabs.Tab>Theming</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock
code={`import { Text } from '@backstage/canon';
<Text />
`}
/>
</Tabs.Panel>
<Tabs.Panel>
We recommend starting with our [global tokens](/theme/theming) to customize the library and align it with
your brand. For additional flexibility, you can use the provided class names for each element listed below.
<CodeBlock
code={`<Text className="text" />`}
/>
</Tabs.Panel>
</Tabs.Root>
## API reference
<PropsTable
data={{
variant: {
type: ['display', 'title1', 'title2', 'title3', 'title4', 'title5'],
responsive: true,
},
weight: {
type: ['regular', 'bold'],
responsive: true,
},
children: {
type: 'ReactNode',
responsive: false,
},
className: {
type: 'string',
responsive: false,
},
style: {
type: 'CSSProperties',
responsive: false,
},
}}
/>
## Examples
### All variants
The `Text` component has a `variant` prop that can be used to change the
appearance of the text.
<Snippet
open
preview={<TextAllVariants />}
code={`<Stack gap="md">
<Text variant="subtitle" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="body" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="caption" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="label" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
</Stack>`}
/>
### All weights
The `Text` component has a `weight` prop that can be used to change the
appearance of the text.
<Snippet
open
preview={<TextAllWeights />}
code={`<Stack gap="md">
<Text weight="regular" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text weight="bold" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
</Stack>`}
/>
### Responsive
You can also use the `variant` prop to change the appearance of the text based
on the screen size.
<Snippet
open
preview={<TextResponsive />}
code={`<Text variant={{ xs: 'label', md: 'body' }}>Responsive</Text>`}
/>
@@ -0,0 +1,6 @@
.pageContainer {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 24px 64px;
}
+16
View File
@@ -0,0 +1,16 @@
import type { Metadata } from 'next';
import styles from './layout.module.css';
export const metadata: Metadata = {
title: 'Canon',
description: 'UI library for Backstage',
};
export default function DocsLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <div className={styles.pageContainer}>{children}</div>;
}
+64
View File
@@ -0,0 +1,64 @@
import { CodeBlock } from '@/components/CodeBlock';
<img
src="header.png"
style={{ width: '100%', marginBottom: '32px', 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.
## Installation
### 1. Install canon
Install Canon using a package manager.
<CodeBlock lang="shell" code={`npm install @backstage/canon`} />
### 2. Import the css files
Import the global CSS file at the root of your application.
```tsx
import '@backstage/canon/css/core.css';
import '@backstage/canon/css/components.css';
```
### 3. Add the theme provider
Add the theme provider to your application.
```tsx
import { ThemeProvider } from '@backstage/canon';
<ThemeProvider>
<App />
</ThemeProvider>;
```
### 4. Start building ✨
Now you can start building your plugin using the new design system.
```tsx
import { Stack, Button } from '@backstage/canon';
<Stack>
<Text>Hello World</Text>
<Button>Click me</Button>
</Stack>;
```
## Roadmap
You can check our roadmap [on GitHub](https://github.com/orgs/backstage/projects/10). We'll do our best to keep you updated on the progress.
## Next steps
Now that you have the basics down, you can start building your plugin using the new design system.
Please familiarise yourself first with our theming principles. This will help you understand the core concepts of the design system.
If you have any questions, please reach out to us on [Discord](https://discord.gg/MUpMjP2).
@@ -0,0 +1,16 @@
import { CodeBlock } from '@/components/CodeBlock';
import { IconLibrary } from '@/components/IconLibrary';
# Iconography
All our default icons are provided by [Remix Icon](https://remixicon.com/). We
don't import all icons to reduce the bundle size but we cherry pick a nice
selection for you to use in your application. The list of names is set down
below. To use an icon, you can use the `Icon` component and pass the name of
the icon you want to use.
<CodeBlock title="Icon component" code={`<Icon name="heart" />`} />
## Icon library
<IconLibrary />
@@ -0,0 +1,42 @@
import { LayoutComponents } from '@/components/LayoutComponents';
import { CodeBlock } from '@/components/CodeBlock';
# Layout
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.
## Layout Components
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.
<CodeBlock
title="Layout components"
code={`<Stack direction="column" gap="md">
<Box>Hello World</Box>
<Inline gap="sm">
<Box>Project 1</Box>
<Box>Project 2</Box>
</Inline>
</Stack>
`}
/>
<LayoutComponents />
## Layout Helpers
Sometimes you want to use global tokens dynamically outside of React
components. To help you with that we would like to provide a set of helpers
that you can use in your code. These helpers are not available just yet but we
are working on it.
@@ -0,0 +1,123 @@
import * as Table from '@/components/Table';
import { Chip } from '@/components/Chip';
import { CodeBlock } from '@/components/CodeBlock';
# Responsive
Canon is built on a responsive design system, meaning that the components are
designed to adapt to different screen sizes. By default we offer a set of
breakpoints that you can use to create responsive components.
## Breakpoints
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Breakpoint prefix</Table.HeaderCell>
<Table.HeaderCell>Minimum width</Table.HeaderCell>
<Table.HeaderCell>CSS</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>xs</Chip>
</Table.Cell>
<Table.Cell>
<Chip>0px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`{ ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>sm</Chip>
</Table.Cell>
<Table.Cell>
<Chip>640px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 640px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>md</Chip>
</Table.Cell>
<Table.Cell>
<Chip>768px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 768px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>lg</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1024px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1024px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>xl</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1280px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1280px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>2xl</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1536px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1536px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
## Responsive components
Canon components are designed to be responsive, meaning that they will adapt
to different screen sizes. Not every component is responsive, but the ones
that are will have a prop to control the responsive behavior.
The behaviour is the same for each component. For each prop, instead of adding
the value, you add an object with the value and the breakpoint prefix.
<CodeBlock
code={`// Fixed value
<Button size="small">Button</Button>
// Responsive value
<Button size={{ xs: 'small', md: 'medium' }}>Button</Button>`} />
## How to update breakpoints
The set of keys are not to be changed, but you can update the minimum width of
each breakpoint in the theme provider.
<CodeBlock
code={`<CanonProvider breakpoints={{
xs: 0,
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1536,
}} />`}
/>
@@ -0,0 +1,220 @@
import { CodeBlock } from '@/components/CodeBlock';
import * as Table from '@/components/Table';
import { Chip } from '@/components/Chip';
# Theming
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.
## Light & Dark modes
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.
## How to create your own theme
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.
<CodeBlock
lang="css"
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-regular: '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-regular: 'Geist', serif;
--canon-font-mono: 'Monospace', monospace;
/* ... other values */
}
`}
/>
## Colors
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.
<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>
## Typography
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.
<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-regular</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>
## Spacing
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.
<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>
@@ -0,0 +1,66 @@
import { HeadingAllVariants } from '@/snippets/heading';
import { TextAllVariants } from '@/snippets/text';
import { Snippet } from '@/components/Snippet';
# Typography
Canon offers a suite of typography components designed to seamlessly align
with the rest of your Backstage instance. While you can customize their
appearance to match your brand, the underlying API remains consistent and
unchanged. Each component is built on a responsive structure, allowing you to
define different typography values for various breakpoints.
## Headings
Headings are used to structure the content of your page. They are used to
create a hierarchy of information and to make the content more readable. The
best way to use add these headings to your page is to import the [Heading
component](?path=/docs/components-heading--docs).
<Snippet
py={2}
open
preview={<HeadingAllVariants />}
code={`<Stack gap="md">
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Stack>`}
/>
## Text
Canon provides four distinct text variants, each offering different font sizes
carefully designed to cover the majority of use cases. These variants are
versatile and can be paired with regular and bold of font weights. You can use
the [Text component](?path=/docs/components-text--docs) to add text to your
page.
<Snippet
open
preview={<TextAllVariants />}
code={`<Stack gap="md">
<Text variant="subtitle" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="body" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="caption" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="label" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
</Stack>`}
/>
@@ -0,0 +1,82 @@
'use client';
import { ReactNode } from 'react';
import { Grid, Stack, Text } from '../../../../../packages/canon';
import { screenSizes } from '@/utils/data';
import { Frame } from '@/components/Frame';
import { usePlayground } from '@/utils/playground-context';
import { ButtonPlayground } from '@/snippets/button';
import { CheckboxPlayground } from '@/snippets/checkbox';
import { HeadingPlayground } from '@/snippets/heading';
import { TextPlayground } from '@/snippets/text';
import styles from './styles.module.css';
export default function PlaygroundPage() {
const { selectedScreenSizes } = usePlayground();
const filteredScreenSizes = screenSizes.filter(item =>
selectedScreenSizes.includes(item.slug),
);
if (filteredScreenSizes.length === 0) {
return (
<div className={styles.containerEmpty}>
<Content />
</div>
);
}
return (
<div className={styles.container}>
{filteredScreenSizes.map(screenSize => (
<div
className={styles.breakpointContainer}
style={{ width: screenSize.width }}
key={screenSize.slug}
>
<Text>
{screenSize.title} - {screenSize.width}px
</Text>
<div className={styles.breakpointContent}>
<Frame>
<Content />
</Frame>
</div>
</div>
))}
</div>
);
}
const Content = () => {
const { selectedComponents } = usePlayground();
return (
<Stack gap="xl">
{selectedComponents.find(c => c === 'button') && (
<Line content={<ButtonPlayground />} title="Button" />
)}
{selectedComponents.find(c => c === 'checkbox') && (
<Line content={<CheckboxPlayground />} title="Checkbox" />
)}
{selectedComponents.find(c => c === 'heading') && (
<Line content={<HeadingPlayground />} title="Heading" />
)}
{selectedComponents.find(c => c === 'text') && (
<Line content={<TextPlayground />} title="Text" />
)}
</Stack>
);
};
const Line = ({ content, title }: { content: ReactNode; title: string }) => {
return (
<Grid gap={{ xs: 'xs', md: 'xl' }}>
<Grid.Item colSpan={2}>
<Text>{title}</Text>
</Grid.Item>
<Grid.Item colSpan={10}>{content}</Grid.Item>
</Grid>
);
};
@@ -0,0 +1,36 @@
.container {
height: 100vh;
display: flex;
flex-direction: row;
gap: 24px;
overflow-x: scroll;
}
.containerEmpty {
padding: 78px 40px;
}
.breakpointContainer {
flex-shrink: 0;
margin-top: 40px;
height: calc(100vh - 86px);
display: flex;
flex-direction: column;
gap: 8px;
&:first-child {
margin-left: 48px;
}
&:last-child {
margin-right: 48px;
}
}
.breakpointContent {
height: 100%;
border-radius: 4px;
border: 1px solid var(--canon-border-base);
background-color: var(--canon-background-base);
padding: 16px;
}
+33
View File
@@ -0,0 +1,33 @@
body {
display: flex;
flex-direction: row;
background-color: var(--canon-background);
color: var(--canon-text-primary);
transition: background-color 0.2s ease-in-out;
}
iframe {
border: none;
width: 100%;
}
.shiki {
margin: 0;
}
.shiki,
.shiki span {
background-color: transparent !important;
font-family: var(--canon-font-monospace);
font-size: 0.875rem;
line-height: 1.7;
}
[data-theme='dark'] .shiki,
[data-theme='dark'] .shiki span {
color: var(--shiki-dark) !important;
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
+4
View File
@@ -0,0 +1,4 @@
<svg width="640" height="640" viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M370.766 153.765C377.563 166.716 371.236 182.114 359.357 190.647C323.005 216.758 299.326 259.411 299.326 307.597C299.326 365.997 334.107 416.27 384.077 438.817C395.235 443.852 403.478 454.368 403.478 466.609V640H382.864C224.433 640 96 511.517 96 353.025L96 20.6143C96 9.22934 105.229 0 116.614 0V0C226.982 3.14552e-06 322.792 62.3523 370.766 153.765Z" fill="black"/>
<path d="M438.772 411.19C496.335 411.19 543 364.454 543 306.803C543 249.152 496.335 202.417 438.772 202.417C381.208 202.417 334.543 249.152 334.543 306.803C334.543 364.454 381.208 411.19 438.772 411.19Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 702 B

+43
View File
@@ -0,0 +1,43 @@
import type { Metadata } from 'next';
import { Sidebar } from '../components/Sidebar';
import styles from './page.module.css';
import { Providers } from './providers';
import './globals.css';
import '/public/core.css';
import '/public/components.css';
import '/public/backstage.css';
export const metadata: Metadata = {
title: 'Canon',
description: 'UI library for Backstage',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// const cookieStore = await cookies();
// const theme = cookieStore.get('theme')?.value || 'light';
// const themeName = cookieStore.get('theme-name')?.value || 'default';
return (
<html
lang="en"
data-theme="light"
data-theme-name="default"
suppressHydrationWarning
>
<body>
<Providers>
<div className={styles.global}>
<Sidebar />
<div className={styles.container}>{children}</div>
</div>
</Providers>
</body>
</html>
);
}
+44
View File
@@ -0,0 +1,44 @@
.global {
width: 100%;
}
.container {
padding-left: 300px;
width: 100%;
}
.page {
flex: 1;
}
.page p {
color: var(--canon-text-primary);
font-size: 1rem;
line-height: 1.5rem;
margin-top: 0;
margin-bottom: 1rem;
}
.content {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 64px 24px 64px;
}
.header {
width: 100%;
margin-bottom: 64px;
}
.header img {
width: 100%;
height: auto;
}
.pageContainer {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 24px 64px;
}
+13
View File
@@ -0,0 +1,13 @@
'use client';
import { ReactNode } from 'react';
import { CanonProvider } from '../../../packages/canon/src/contexts/canon';
import { PlaygroundProvider } from '@/utils/playground-context';
export const Providers = ({ children }: { children: ReactNode }) => {
return (
<CanonProvider>
<PlaygroundProvider>{children}</PlaygroundProvider>
</CanonProvider>
);
};
@@ -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 styles from './styles.module.css';
export const Banner = ({
variant = 'info',
text,
}: {
variant?: 'info' | 'warning';
text?: string;
}) => {
return (
<div className={`${styles.banner} ${styles[variant]}`}>
<div className={styles.icon}>
<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>
<div className={styles.text}>{text}</div>
</div>
);
};
+16
View File
@@ -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 { Banner } from './Banner';
@@ -0,0 +1,41 @@
.banner {
display: flex;
align-items: center;
font-size: 16px;
line-height: 28px;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
border: 1px solid #e0e0e0;
& > p {
margin: 0;
}
}
.info {
background-color: #f2f2f2;
border-color: #cdcdcd;
color: #888888;
}
.warning {
background-color: #fff2b9;
border-color: #ffd000;
color: #d79927;
}
.icon {
width: 32px;
height: 32px;
background-color: rgba(215, 153, 39, 0.2);
border-radius: 6px;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.text {
line-height: 1.5;
}
+32
View File
@@ -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 './styles.module.css';
export const Chip = ({
children,
head = false,
}: {
children: React.ReactNode;
head?: boolean;
}) => {
return (
<span className={`${styles.chip} ${head ? styles.head : ''}`}>
{children}
</span>
);
};
+16
View File
@@ -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 { Chip } from './Chip';
@@ -0,0 +1,43 @@
/*
* 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.
*/
.chip {
display: inline-flex;
align-items: center;
font-family: monospace;
font-size: 13px;
border-radius: 6px;
padding: 0px 8px;
height: 24px;
margin-right: 4px;
background-color: #f0f0f0;
color: #5d5d5d;
transition: background-color 0.2s ease-in-out;
}
.head {
background-color: #eaf2fd;
color: #2563eb;
}
[data-theme='dark'] .chip {
background-color: #2c2c2c;
color: #fff;
}
[data-theme='dark'] .chip.head {
background-color: #26417b;
}
@@ -0,0 +1,32 @@
import React from 'react';
import type { BundledLanguage } from 'shiki';
import { codeToHtml } from 'shiki';
import { Text } from '../../../../packages/canon/src/components/Text';
import styles from './styles.module.css';
interface CodeBlockProps {
lang?: BundledLanguage;
title?: string;
code?: string;
}
export async function CodeBlock({ lang = 'tsx', title, code }: CodeBlockProps) {
const out = await codeToHtml(code || '', {
lang: lang,
themes: {
light: 'min-light',
dark: 'min-dark',
},
});
return (
<div className={styles.codeBlock}>
{title && (
<div className={styles.title}>
<Text variant="body">{title}</Text>
</div>
)}
<div dangerouslySetInnerHTML={{ __html: out }} className={styles.code} />
</div>
);
}
@@ -0,0 +1,26 @@
.codeBlock {
border-radius: 4px;
border: 1px solid var(--canon-border-base);
position: relative;
background: transparent;
overflow-x: auto;
font-family: var(--canon-font-monospace);
background-color: #fff;
transition: background-color 0.2s ease-in-out;
margin-bottom: 1rem;
}
[data-theme='dark'] .codeBlock {
background-color: #121212;
}
.title {
border-bottom: 1px solid var(--canon-border-base);
padding: 12px 20px;
font-size: 0.875rem;
color: var(--canon-text-secondary);
}
.code {
padding: 20px;
}
@@ -0,0 +1,31 @@
/*
* 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 Columns = ({
children,
style,
}: {
children: React.ReactNode;
style?: React.CSSProperties;
}) => {
return (
<div className="columns" style={style}>
{children}
</div>
);
};
@@ -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 { Columns } from './Columns';
@@ -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.
*/
.columns {
display: grid;
grid-template-columns: repeat(3, 1fr);
row-gap: 20px;
column-gap: 80px;
}
@@ -0,0 +1,5 @@
import styles from './styles.module.css';
export const DecorativeBox = () => {
return <div className={styles.box} />;
};
@@ -0,0 +1,8 @@
.box {
min-width: 64px;
min-height: 64px;
background-color: #eaf2fd;
border-radius: 4px;
box-shadow: 0 0 0 1px #2563eb;
background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%232563eb' fill-opacity='0.3' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E");
}
+33
View File
@@ -0,0 +1,33 @@
/* eslint-disable @next/next/no-css-tags */
'use client';
import { useEffect, useState } from 'react';
import ReactFrame from 'react-frame-component';
export const Frame = ({ children }: { children: React.ReactNode }) => {
const [show, setShow] = useState(false);
useEffect(() => {
setShow(true);
}, []);
if (!show) return null;
return (
<ReactFrame
loading="lazy"
style={{ width: '100%', height: '100%' }}
initialContent={`<!DOCTYPE html><html data-theme="light"><head></head><body><div class="frame-root"></div></body></html>`}
mountTarget=".frame-root"
head={
<>
<link rel="stylesheet" href="/core.css" />
<link rel="stylesheet" href="/components.css" />
<link rel="stylesheet" href="/backstage.css" />
</>
}
>
{children}
</ReactFrame>
);
};
@@ -0,0 +1,43 @@
import styles from './styles.module.css';
import { Text } from '../../../../packages/canon/src/components/Text';
import { Icon } from '../../../../packages/canon/src/components/Icon';
interface BaseUIProps {
href: string;
}
export const BaseUI = ({ href }: BaseUIProps) => {
return (
<div className={styles.container}>
<svg
width="17"
height="30"
viewBox="0 0 17 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={styles.icon}
>
<path d="M8 12.8V15V26C3.58172 26 0 22.0601 0 17.2V15V4C4.41828 4 8 7.93989 8 12.8Z" />
<path d="M9.5001 10.0154C9.2245 9.99843 9 10.2239 9 10.5V26.0001C13.4183 26.0001 17 22.4184 17 18.0001C17 13.7498 13.6854 10.2736 9.5001 10.0154Z" />
</svg>
<div className={styles.content}>
<Text variant="subtitle" weight="bold">
Base UI
</Text>
<div className={styles.description}>
<Text variant="subtitle">
This component is using Base UI under the hood. While most of the
original props are available, we have made some changes to the API
to fit our needs.
</Text>
</div>
{href && (
<a className={styles.button} href={href} target="_blank">
Discover more
<Icon name="externalLink" />
</a>
)}
</div>
</div>
);
};
@@ -0,0 +1,42 @@
.container {
display: flex;
background-color: var(--canon-surface-1);
padding: var(--canon-spacing-md);
border-radius: var(--canon-border-radius-xs);
border: 1px solid var(--canon-border-base);
margin-bottom: var(--canon-spacing-md);
gap: var(--canon-spacing-md);
transition: background-color 0.2s ease-in-out;
}
.icon path {
fill: var(--canon-text-primary);
}
.content {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.description {
max-width: 700px;
}
.button {
all: unset;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-color: var(--canon-surface-2);
border: 1px solid var(--canon-border-base);
border-radius: var(--canon-border-radius-xs);
padding: 0 var(--canon-spacing-xs);
height: 28px;
border-radius: 100px;
margin-top: var(--canon-spacing-xs);
font-size: var(--canon-font-size-body);
color: var(--canon-text-primary);
gap: var(--canon-spacing-2xs);
}
@@ -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 { Icon } from '../../../../packages/canon/src/components/Icon';
import type { IconNames } from '../../../../packages/canon/src/components/Icon';
import { Text } from '../../../../packages/canon/src/components/Text';
import { icons } from '../../../../packages/canon/src/components/Icon/icons';
import styles from './styles.module.css';
export const IconLibrary = () => {
const list = Object.keys(icons);
return (
<div className={styles.library}>
{list.map(icon => (
<div key={icon} className={styles.item}>
<div className={styles.icon}>
<Icon name={icon as IconNames} />
</div>
<Text variant="body">{icon}</Text>
</div>
))}
</div>
);
};
@@ -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 { IconLibrary } from './IconLibrary';
@@ -0,0 +1,28 @@
.library {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 1rem;
}
.item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.icon {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
height: 80px;
border: 1px solid #d3d3d3;
border-radius: 0.5rem;
background-color: var(--canon-background);
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: var(--canon-surface-1);
}
}
@@ -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.
*/
.layoutComponents {
display: flex;
justify-content: flex-start;
gap: 1rem;
flex-wrap: wrap;
& .box {
display: flex;
flex-direction: column;
width: calc(33.33% - 0.67rem);
margin-bottom: 1rem;
align-items: flex-start;
}
& .content {
flex: none;
background-color: #fff;
border-radius: 4px;
width: 100%;
height: 180px;
transition: all 0.2s ease-in-out;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
&:hover {
transform: translateY(-4px);
}
}
& .title {
font-size: 16px;
transition: color 0.2s ease-in-out;
margin-bottom: 0.25rem;
}
& .description {
font-size: 16px;
color: #9e9e9e;
}
}
@@ -0,0 +1,60 @@
import React from 'react';
import { BoxSvg } from './svgs/box';
import { StackSvg } from './svgs/stack';
import { GridSvg } from './svgs/grid';
import { InlineSvg } from './svgs/inline';
import { ContainerSvg } from './svgs/container';
import styles from './LayoutComponents.module.css';
import Link from 'next/link';
export const LayoutComponents = () => {
return (
<div className={styles.layoutComponents}>
<div className={styles.box}>
<Link className={styles.content} href="/components/box">
<BoxSvg />
</Link>
<div className={styles.title}>Box</div>
<div className={styles.description}>
The most basic layout component
</div>
</div>
<div className={styles.box}>
<Link className={styles.content} href="/components/stack">
<StackSvg />
</Link>
<div className={styles.title}>Stack</div>
<div className={styles.description}>
Arrange your components vertically
</div>
</div>
<div className={styles.box}>
<Link className={styles.content} href="/components/grid">
<GridSvg />
</Link>
<div className={styles.title}>Grid</div>
<div className={styles.description}>
Arrange your components in a grid
</div>
</div>
<div className={styles.box}>
<Link className={styles.content} href="/components/inline">
<InlineSvg />
</Link>
<div className={styles.title}>Inline</div>
<div className={styles.description}>
Arrange your components in a row
</div>
</div>
<div className={styles.box}>
<Link className={styles.content} href="/components/container">
<ContainerSvg />
</Link>
<div className={styles.title}>Container</div>
<div className={styles.description}>
A container for your components
</div>
</div>
</div>
);
};
@@ -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 { LayoutComponents } from './LayoutComponents';
@@ -0,0 +1,54 @@
/*
* 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 BoxSvg = () => {
return (
<svg
width="100"
height="60"
viewBox="0 0 100 60"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="100" height="60" rx="4" fill="black" fillOpacity="0.06" />
<path
d="M94.5 0.5H96C97.933 0.5 99.5 2.067 99.5 4V5.5"
stroke="#4765FF"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M99.5 54.5L99.5 56C99.5 57.933 97.933 59.5 96 59.5L94.5 59.5"
stroke="#4765FF"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.5 59.5L4 59.5C2.067 59.5 0.5 57.933 0.5 56L0.5 54.5"
stroke="#4765FF"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M0.5 5.5L0.5 4C0.5 2.067 2.067 0.5 4 0.5L5.5 0.500001"
stroke="#4765FF"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
@@ -0,0 +1,372 @@
/*
* 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 ContainerSvg = () => {
return (
<svg
width="126"
height="111"
viewBox="0 0 126 111"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<mask
id="mask0_1922_1559"
maskUnits="userSpaceOnUse"
x="0"
y="0"
width="13"
height="111"
>
<rect width="13" height="111" fill="url(#paint0_linear_1922_1559)" />
</mask>
<g mask="url(#mask0_1922_1559)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 44.7563L13.0259 51.6995L12.5259 52.5655L0.5 45.6224L1 44.7563Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 50.5655L13.0259 57.5087L12.5259 58.3747L0.5 51.4316L1 50.5655Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 56.3747L13.0259 63.3179L12.5259 64.1839L0.5 57.2408L1 56.3747Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 62.1839L13.0259 69.1271L12.5259 69.9931L0.5 63.05L1 62.1839Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 67.9931L13.0259 74.9363L12.5259 75.8023L0.5 68.8592L1 67.9931Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 73.8023L13.0259 80.7455L12.5259 81.6116L0.5 74.6684L1 73.8023Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 79.6115L13.0259 86.5547L12.5259 87.4208L0.5 80.4776L1 79.6115Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 85.4208L13.0259 92.3639L12.5259 93.23L0.5 86.2868L1 85.4208Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 91.23L13.0259 98.1731L12.5259 99.0392L0.5 92.096L1 91.23Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 97.0392L13.0259 103.982L12.5259 104.848L0.5 97.9052L1 97.0392Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 102.848L13.0259 109.792L12.5259 110.658L0.5 103.714L1 102.848Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 108.658L13.0259 115.601L12.5259 116.467L0.5 109.524L1 108.658Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 38.9471L13.0259 45.8903L12.5259 46.7563L0.5 39.8131L1 38.9471Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 33.1379L13.0259 40.0811L12.5259 40.9471L0.5 34.0039L1 33.1379Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 27.3287L13.0259 34.2719L12.5259 35.1379L0.5 28.1947L1 27.3287Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 21.5195L13.0259 28.4627L12.5259 29.3287L0.5 22.3855L1 21.5195Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 15.7103L13.0259 22.6535L12.5259 23.5195L0.5 16.5763L1 15.7103Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 9.90109L13.0259 16.8443L12.5259 17.7103L0.5 10.7671L1 9.90109Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 4.09188L13.0259 11.0351L12.5259 11.9011L0.5 4.95791L1 4.09188Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 -1.71732L13.0259 5.22586L12.5259 6.09188L0.5 -0.851298L1 -1.71732Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 -7.52653L13.0259 -0.583351L12.5259 0.282675L0.5 -6.66051L1 -7.52653Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 131L0 -17H1L1 131H0Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 131L12 -17H13L13 131H12Z"
fill="#4765FF"
/>
</g>
<rect
x="13"
y="25.5"
width="100"
height="60"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<mask
id="mask1_1922_1559"
// style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="113"
y="0"
width="13"
height="111"
>
<rect
x="113"
width="13"
height="111"
fill="url(#paint1_linear_1922_1559)"
/>
</mask>
<g mask="url(#mask1_1922_1559)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 44.7563L126.026 51.6995L125.526 52.5655L113.5 45.6224L114 44.7563Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 50.5655L126.026 57.5087L125.526 58.3747L113.5 51.4316L114 50.5655Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 56.3747L126.026 63.3179L125.526 64.1839L113.5 57.2408L114 56.3747Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 62.1839L126.026 69.1271L125.526 69.9931L113.5 63.05L114 62.1839Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 67.9931L126.026 74.9363L125.526 75.8023L113.5 68.8592L114 67.9931Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 73.8023L126.026 80.7455L125.526 81.6116L113.5 74.6684L114 73.8023Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 79.6115L126.026 86.5547L125.526 87.4208L113.5 80.4776L114 79.6115Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 85.4208L126.026 92.3639L125.526 93.23L113.5 86.2868L114 85.4208Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 91.23L126.026 98.1731L125.526 99.0392L113.5 92.096L114 91.23Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 97.0392L126.026 103.982L125.526 104.848L113.5 97.9052L114 97.0392Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 102.848L126.026 109.792L125.526 110.658L113.5 103.714L114 102.848Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 108.658L126.026 115.601L125.526 116.467L113.5 109.524L114 108.658Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 38.9471L126.026 45.8903L125.526 46.7563L113.5 39.8131L114 38.9471Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 33.1379L126.026 40.0811L125.526 40.9471L113.5 34.0039L114 33.1379Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 27.3287L126.026 34.2719L125.526 35.1379L113.5 28.1947L114 27.3287Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 21.5195L126.026 28.4627L125.526 29.3287L113.5 22.3855L114 21.5195Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 15.7103L126.026 22.6535L125.526 23.5195L113.5 16.5763L114 15.7103Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 9.90109L126.026 16.8443L125.526 17.7103L113.5 10.7671L114 9.90109Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 4.09188L126.026 11.0351L125.526 11.9011L113.5 4.95791L114 4.09188Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 -1.71732L126.026 5.22586L125.526 6.09188L113.5 -0.851298L114 -1.71732Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M114 -7.52653L126.026 -0.583351L125.526 0.282675L113.5 -6.66051L114 -7.52653Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M113 131L113 -17H114L114 131H113Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M125 131L125 -17H126L126 131H125Z"
fill="#4765FF"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_1922_1559"
x1="6.5"
y1="0"
x2="6.5"
y2="111"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.05" stopColor="white" stopOpacity="0" />
<stop offset="0.2" stopColor="white" />
<stop offset="0.8" stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
<linearGradient
id="paint1_linear_1922_1559"
x1="119.5"
y1="0"
x2="119.5"
y2="111"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.05" stopColor="white" stopOpacity="0" />
<stop offset="0.2" stopColor="white" />
<stop offset="0.8" stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
};
@@ -0,0 +1,91 @@
/*
* 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 GridSvg = () => {
return (
<svg
width="166"
height="61"
viewBox="0 0 166 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
width="52"
height="61"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M53.5 30L64.5 30V31L53.5 31L53.5 30Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M64.5 24.5C64.5 24.2239 64.7239 24 65 24C65.2761 24 65.5 24.2239 65.5 24.5L65.5 36.5C65.5 36.7761 65.2761 37 65 37C64.7239 37 64.5 36.7761 64.5 36.5L64.5 24.5Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M52.5 24.5C52.5 24.2239 52.7239 24 53 24C53.2761 24 53.5 24.2239 53.5 24.5L53.5 36.5C53.5 36.7761 53.2761 37 53 37C52.7239 37 52.5 36.7761 52.5 36.5L52.5 24.5Z"
fill="#4765FF"
/>
<rect
x="65.5"
y="0.5"
width="100"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M115 36.5L115 25.5L116 25.5L116 36.5L115 36.5Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M109.5 25.5C109.224 25.5 109 25.2761 109 25V25C109 24.7239 109.224 24.5 109.5 24.5L121.5 24.5C121.776 24.5 122 24.7239 122 25V25C122 25.2761 121.776 25.5 121.5 25.5L109.5 25.5Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M109.5 36.5C109.224 36.5 109 36.2761 109 36V36C109 35.7239 109.224 35.5 109.5 35.5L121.5 35.5C121.776 35.5 122 35.7239 122 36V36C122 36.2761 121.776 36.5 121.5 36.5L109.5 36.5Z"
fill="#4765FF"
/>
<rect
x="65.5"
y="36.5"
width="100"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
</svg>
);
};
@@ -0,0 +1,92 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
export const InlineSvg = () => {
return (
<svg
width="159"
height="56"
viewBox="0 0 159 56"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="24" height="24" rx="4" fill="black" fillOpacity="0.06" />
<rect
x="32"
width="24"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<rect
x="64"
width="24"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<rect
x="96"
width="24"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<rect
x="128"
width="24"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<rect
y="32"
width="24"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<rect
x="32"
y="32"
width="24"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<rect
x="64"
y="32"
width="24"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<path
d="M12 12H138.811C141.238 12 141.569 15.5106 139.185 15.9647L12.8145 40.0353C10.4306 40.4894 10.762 44 13.1888 44H77"
stroke="#4765FF"
/>
</svg>
);
};
@@ -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';
export const StackSvg = () => {
return (
<svg
width="100"
height="96"
viewBox="0 0 100 96"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="100" height="24" rx="4" fill="black" fillOpacity="0.06" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M49.5 36L49.5 25L50.5 25L50.5 36L49.5 36Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M44 25C43.7239 25 43.5 24.7761 43.5 24.5V24.5C43.5 24.2239 43.7239 24 44 24L56 24C56.2761 24 56.5 24.2239 56.5 24.5V24.5C56.5 24.7761 56.2761 25 56 25L44 25Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M44 36C43.7239 36 43.5 35.7761 43.5 35.5V35.5C43.5 35.2239 43.7239 35 44 35L56 35C56.2761 35 56.5 35.2239 56.5 35.5V35.5C56.5 35.7761 56.2761 36 56 36L44 36Z"
fill="#4765FF"
/>
<rect
y="36"
width="100"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M49.5 72L49.5 61L50.5 61L50.5 72L49.5 72Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M44 61C43.7239 61 43.5 60.7761 43.5 60.5V60.5C43.5 60.2239 43.7239 60 44 60L56 60C56.2761 60 56.5 60.2239 56.5 60.5V60.5C56.5 60.7761 56.2761 61 56 61L44 61Z"
fill="#4765FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M44 72C43.7239 72 43.5 71.7761 43.5 71.5V71.5C43.5 71.2239 43.7239 71 44 71L56 71C56.2761 71 56.5 71.2239 56.5 71.5V71.5C56.5 71.7761 56.2761 72 56 72L44 72Z"
fill="#4765FF"
/>
<rect
y="72"
width="100"
height="24"
rx="4"
fill="black"
fillOpacity="0.06"
/>
</svg>
);
};
@@ -0,0 +1,47 @@
import React from 'react';
import * as Table from '../Table';
import { Chip } from '../Chip';
// Define a more specific type for the data object
type PropData = {
type: string | string[];
responsive: boolean;
};
// Modify the PropsTable component to use the new type
export const PropsTable = <T extends Record<string, PropData>>({
data,
}: {
data: T;
}) => {
return (
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Type</Table.HeaderCell>
<Table.HeaderCell>Responsive</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
{Object.keys(data).map(n => (
<Table.Row key={n}>
<Table.Cell>
<Chip head>{n}</Chip>
</Table.Cell>
<Table.Cell>
{Array.isArray(data[n].type) ? (
data[n].type.map(t => <Chip key={t}>{t}</Chip>)
) : (
<Chip>{data[n].type}</Chip>
)}
</Table.Cell>
<Table.Cell>
<Chip>{data[n].responsive ? 'Yes' : 'No'}</Chip>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};
@@ -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 { PropsTable } from './PropsTable';
@@ -0,0 +1,53 @@
/*
* 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 { RoadmapItem } from './list';
export const Roadmap = ({ list }: { list: RoadmapItem[] }) => {
const orderList = ['inProgress', 'notStarted', 'completed'];
return (
<div className="roadmap">
{list
.sort(
(a, b) => orderList.indexOf(a.status) - orderList.indexOf(b.status),
)
.map(Item)}
</div>
);
};
const Item = ({
title,
status = 'notStarted',
}: {
title: string;
status: 'notStarted' | 'inProgress' | 'inReview' | 'completed';
}) => {
return (
<div className={['roadmap-item', status].join(' ')}>
<div className="left">
<div className="dot" />
<div className="title">{title}</div>
</div>
<span className="pill">
{status === 'notStarted' && 'Not Started'}
{status === 'inProgress' && 'In Progress'}
{status === 'inReview' && 'Ready for Review'}
{status === 'completed' && 'Completed'}
</span>
</div>
);
};
@@ -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 { Roadmap } from './Roadmap';
+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.
*/
export type RoadmapItem = {
title: string;
status: 'notStarted' | 'inProgress' | 'inReview' | 'completed';
};
export const list: RoadmapItem[] = [
{
title: 'Remove Vanilla Extract and use pure CSS instead',
status: 'inProgress',
},
{
title: 'Add collapsing across breakpoints for the Inline component',
status: 'notStarted',
},
{
title: 'Add reversing the order for the Inline component',
status: 'notStarted',
},
{
title: 'Set up Storybook',
status: 'completed',
},
{
title: 'Set up iconography',
status: 'completed',
},
{
title: 'Set up global tokens',
status: 'inProgress',
},
{
title: 'Set up theming system',
status: 'inProgress',
},
{
title: 'Create first pass at box component',
status: 'completed',
},
{
title: 'Create first pass at stack component',
status: 'completed',
},
{
title: 'Create first pass at inline component',
status: 'completed',
},
];
@@ -0,0 +1,100 @@
.roadmap {
display: flex;
flex-direction: column;
}
.roadmap .roadmap-item {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e0e0e0;
padding: 8px 0px;
}
.roadmap .roadmap-item .left {
display: flex;
align-items: center;
padding-left: 12px;
gap: 12px;
}
.roadmap .roadmap-item .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #e0e0e0;
}
.roadmap .roadmap-item.notStarted {
color: #000;
}
.roadmap .roadmap-item.inProgress {
color: #000;
}
.roadmap .roadmap-item.inReview {
color: #000;
}
.roadmap .roadmap-item.completed .title {
color: #a2a2a2;
text-decoration: line-through;
}
.roadmap .roadmap-item.notStarted .dot {
background-color: #d1d1d1;
}
.roadmap .roadmap-item.inProgress .dot {
background-color: #ffd000;
}
.roadmap .roadmap-item.inReview .dot {
background-color: #4ed14a;
}
.roadmap .roadmap-item.completed .dot {
background-color: #4ed14a;
}
.roadmap .roadmap-item .title {
font-size: 16px;
}
.roadmap .roadmap-item .pill {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0px 8px;
border-radius: 40px;
font-size: 12px;
font-weight: 600;
margin-left: 8px;
border-style: solid;
border-width: 1px;
}
.roadmap .roadmap-item.notStarted .pill {
background-color: #f2f2f2;
border-color: #cdcdcd;
color: #888888;
}
.roadmap .roadmap-item.inProgress .pill {
background-color: #fff2b9;
border-color: #ffd000;
color: #d79927;
}
.roadmap .roadmap-item.inReview .pill {
background-color: #d7f9d7;
border-color: #4ed14a;
color: #3a9837;
}
.roadmap .roadmap-item.completed .pill {
background-color: #d7f9d7;
border-color: #4ed14a;
color: #3a9837;
}
@@ -0,0 +1,67 @@
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 100vh;
color: var(--canon-text-primary);
background-color: var(--canon-surface-1);
border-right: 1px solid var(--canon-border-base);
padding-left: 20px;
padding-right: 20px;
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
overflow: hidden;
}
.logo path {
fill: var(--canon-text-primary);
}
.content {
padding-top: 20px;
}
.actions {
display: flex;
flex-direction: row;
gap: 12px;
margin-top: 20px;
margin-bottom: 12px;
}
.menu {
display: flex;
flex-direction: row;
position: relative;
}
.menu a {
display: flex;
text-decoration: none;
height: 28px;
align-items: center;
}
.section {
width: 100%;
}
.line {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 28px;
padding: 0 12px;
border-radius: 4px;
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: var(--canon-surface-2);
}
}
.line.active {
background-color: var(--canon-surface-2);
}
@@ -0,0 +1,87 @@
.tabs {
border-radius: 0.375rem;
width: 100%;
background-color: var(--canon-surface-2);
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}
.tabsTheme {
width: 142px;
border-radius: 0.375rem;
background-color: var(--canon-surface-2);
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}
.list {
display: flex;
position: relative;
z-index: 0;
padding-inline: 0.25rem;
gap: 0.25rem;
}
.tab {
display: flex;
align-items: center;
justify-content: center;
border: 0;
margin: 0;
outline: 0;
background: none;
appearance: none;
color: var(--canon-text-secondary) !important;
font-family: inherit;
font-size: 0.75rem;
line-height: 1.25rem;
font-weight: 500;
user-select: none;
height: 2.25rem;
flex: 1;
cursor: pointer;
&[data-selected] {
color: var(--canon-text-primary) !important;
& p {
color: var(--canon-text-primary) !important;
}
}
@media (hover: hover) {
&:hover {
color: var(--canon-text-primary);
}
}
&:focus-visible {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0.25rem 0;
border-radius: 0.25rem;
outline: 2px solid var(--canon-surface-1);
outline-offset: -1px;
}
}
}
.tab p {
color: var(--canon-text-secondary) !important;
}
.indicator {
position: absolute;
z-index: -1;
left: 0;
top: 50%;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
height: 1.8rem;
border-radius: 0.25rem;
background-color: var(--canon-surface-1);
transition-property: translate, width, background-color;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}
@@ -0,0 +1,90 @@
'use client';
import Link from 'next/link';
import { components, overview, layoutComponents, theme } from '@/utils/data';
import { Box } from '../../../../packages/canon/src/components/Box';
import { Text } from '../../../../packages/canon/src/components/Text';
import { motion } from 'framer-motion';
import styles from './Sidebar.module.css';
import { usePathname } from 'next/navigation';
import { Fragment } from 'react';
const data = [
{
title: 'Overview',
content: overview,
url: '',
},
{
title: 'Theme',
content: theme,
url: '/theme',
},
{
title: 'Layout Components',
content: layoutComponents,
url: '/components',
},
{
title: 'Components',
content: components,
url: '/components',
},
];
export const Docs = () => {
const pathname = usePathname();
const isPlayground = pathname.includes('/playground');
return (
<motion.div
className={styles.section}
animate={{
x: isPlayground ? -10 : 0,
opacity: isPlayground ? 0 : 1,
visibility: isPlayground ? 'hidden' : 'visible',
}}
initial={{
x: isPlayground ? -10 : 0,
opacity: isPlayground ? 0 : 1,
visibility: isPlayground ? 'hidden' : 'visible',
}}
transition={{ duration: 0.2 }}
>
{data.map(section => {
return (
<Fragment key={section.title}>
<Box marginTop="lg" marginBottom="2xs">
<Text variant="body" weight="bold">
{section.title}
</Text>
</Box>
{section.content.map(item => {
const isActive = pathname === `${section.url}/${item.slug}`;
return (
<Link
href={`${section.url}/${item.slug}`}
key={item.slug}
className={`${styles.line} ${isActive ? styles.active : ''}`}
>
<Text variant="body">{item.title}</Text>
<Text
variant="body"
style={{ color: 'var(--canon-text-secondary)' }}
>
{item.status === 'alpha' && 'Alpha'}
{item.status === 'beta' && 'Beta'}
{item.status === 'inProgress' && 'In Progress'}
{item.status === 'stable' && 'Stable'}
{item.status === 'deprecated' && 'Deprecated'}
</Text>
</Link>
);
})}
</Fragment>
);
})}
</motion.div>
);
};
@@ -0,0 +1,35 @@
import styles from './Sidebar.module.css';
import { TabsVersion, TabsPages, TabsTheme } from './tabs';
import { Docs } from './docs';
import { Playground } from './playground';
import Link from 'next/link';
export const Sidebar = () => {
return (
<div className={styles.sidebar}>
<div className={styles.content}>
<Link href="/">
<svg
width="89"
height="27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={styles.logo}
>
<path d="M77.414 9.71h3.036v2.992l-.264-.022c.279-.88.748-1.65 1.408-2.31.675-.66 1.533-.99 2.574-.99 1.086 0 1.9.352 2.442 1.056.558.689.836 1.598.836 2.728v7.502H84.41v-6.6c0-.66-.117-1.159-.352-1.496-.234-.338-.623-.506-1.166-.506-.601 0-1.122.271-1.562.814a4.321 4.321 0 0 0-.88 1.848v5.94h-3.036V9.71ZM69.868 21.04c-1.13 0-2.142-.257-3.036-.77a5.547 5.547 0 0 1-2.068-2.09c-.484-.895-.726-1.892-.726-2.992 0-1.115.242-2.112.726-2.992a5.503 5.503 0 0 1 2.068-2.112c.88-.514 1.892-.77 3.036-.77 1.144 0 2.156.256 3.036.77a5.347 5.347 0 0 1 2.046 2.112c.498.88.748 1.87.748 2.97 0 1.114-.25 2.12-.748 3.014a5.343 5.343 0 0 1-2.068 2.09c-.88.513-1.885.77-3.014.77Zm0-2.618c.557 0 1.048-.132 1.474-.396a2.79 2.79 0 0 0 .99-1.144c.234-.499.352-1.064.352-1.694 0-.63-.118-1.188-.352-1.672a2.764 2.764 0 0 0-.99-1.166c-.426-.279-.917-.418-1.474-.418-.558 0-1.05.14-1.474.418a2.764 2.764 0 0 0-.99 1.166c-.22.484-.33 1.041-.33 1.672 0 .63.11 1.195.33 1.694.234.484.564.865.99 1.144.425.264.916.396 1.474.396ZM52.385 9.71h3.036v2.992l-.264-.022c.279-.88.748-1.65 1.408-2.31.675-.66 1.533-.99 2.574-.99 1.085 0 1.9.352 2.442 1.056.557.689.836 1.598.836 2.728v7.502h-3.036v-6.6c0-.66-.117-1.159-.352-1.496-.235-.338-.623-.506-1.166-.506-.601 0-1.122.271-1.562.814a4.321 4.321 0 0 0-.88 1.848v5.94h-3.036V9.71ZM43.904 20.952c-1.026 0-1.87-.308-2.53-.924-.66-.616-.99-1.416-.99-2.398 0-1.115.418-2.01 1.254-2.684.85-.69 2.105-1.034 3.762-1.034h2.75l-.924.352v-.462c0-.572-.176-1.02-.528-1.342-.352-.338-.931-.506-1.738-.506-.69 0-1.386.154-2.09.462-.704.308-1.29.69-1.76 1.144v-2.882c.47-.352 1.1-.66 1.892-.924a7.942 7.942 0 0 1 2.486-.396c1.599 0 2.78.41 3.542 1.232.778.806 1.166 1.914 1.166 3.322v5.742c0 .176.008.352.022.528.03.161.066.322.11.484h-3.014v-3.3l.044 1.386a4.044 4.044 0 0 1-1.32 1.606c-.572.396-1.283.594-2.134.594Zm.858-2.156c.558 0 1.049-.206 1.474-.616.44-.426.77-.954.99-1.584v-.792h-1.562c-.762 0-1.334.146-1.716.44-.381.278-.572.674-.572 1.188 0 .41.125.74.374.99.264.25.602.374 1.012.374ZM34.66 21.04c-1.525 0-2.874-.345-4.047-1.034-1.174-.704-2.09-1.68-2.75-2.926-.646-1.247-.968-2.662-.968-4.246 0-1.584.322-2.992.968-4.224.66-1.232 1.576-2.193 2.75-2.882 1.173-.704 2.522-1.056 4.048-1.056.968 0 1.818.11 2.552.33.748.22 1.356.506 1.826.858v3.498c-.396-.484-.96-.895-1.694-1.232-.719-.338-1.51-.506-2.376-.506-.939 0-1.775.22-2.508.66-.719.44-1.284 1.056-1.694 1.848-.396.792-.594 1.694-.594 2.706 0 1.026.198 1.943.594 2.75.41.792.975 1.408 1.694 1.848.733.44 1.57.66 2.508.66.88 0 1.68-.162 2.398-.484.718-.323 1.276-.726 1.672-1.21v3.41a5.686 5.686 0 0 1-1.87.902c-.704.22-1.54.33-2.508.33ZM11.275 6.271c.276.526.019 1.152-.464 1.498a5.846 5.846 0 0 0 1.004 10.082c.454.205.789.632.789 1.13v6.206a.837.837 0 0 1-.838.837C5.33 26.024.112 20.804.112 14.366V.862C.112.399.487.024.95.024c4.483 0 8.376 2.533 10.325 6.247Z" />
<path d="M14.037 16.729a4.237 4.237 0 0 0 4.234-4.24 4.237 4.237 0 0 0-4.234-4.242 4.237 4.237 0 0 0-4.234 4.241 4.237 4.237 0 0 0 4.234 4.24Z" />
</svg>
</Link>
<div className={styles.actions}>
<TabsVersion />
<TabsTheme />
</div>
<TabsPages />
<div className={styles.menu}>
<Docs />
<Playground />
</div>
</div>
</div>
);
};
@@ -0,0 +1,87 @@
'use client';
import { components } from '@/utils/data';
import { Box } from '../../../../packages/canon/src/components/Box';
import { Checkbox } from '../../../../packages/canon/src/components/Checkbox';
import { Text } from '../../../../packages/canon/src/components/Text';
import { motion } from 'framer-motion';
import styles from './Sidebar.module.css';
import { usePathname } from 'next/navigation';
import { screenSizes } from '@/utils/data';
import { usePlayground } from '@/utils/playground-context';
export const Playground = () => {
const pathname = usePathname();
const isPlayground = pathname.includes('/playground');
const {
selectedScreenSizes,
setSelectedScreenSizes,
selectedComponents,
setSelectedComponents,
} = usePlayground();
const handleComponentCheckboxChange = (slug: string) => {
if (selectedComponents.find(item => item === slug)) {
const res = selectedComponents.filter(item => item !== slug);
setSelectedComponents(res);
} else {
setSelectedComponents([...selectedComponents, slug]);
}
};
const handleCheckboxChange = (slug: string) => {
if (selectedScreenSizes.find(item => item === slug)) {
const res = selectedScreenSizes.filter(item => item !== slug);
setSelectedScreenSizes(res);
} else {
setSelectedScreenSizes([...selectedScreenSizes, slug]);
}
};
return (
<motion.div
className={styles.section}
animate={{
opacity: isPlayground ? 1 : 0,
x: isPlayground ? 0 : 20,
visibility: isPlayground ? 'visible' : 'hidden',
}}
initial={{
opacity: isPlayground ? 1 : 0,
x: isPlayground ? 0 : 20,
visibility: isPlayground ? 'visible' : 'hidden',
}}
transition={{ duration: 0.2 }}
style={{ position: 'absolute' }}
>
<Box marginTop="lg" marginBottom="2xs">
<Text variant="body" weight="bold">
Components
</Text>
</Box>
{components.map(({ slug, title }) => (
<div className={styles.line} key={slug}>
<Text variant="body">{title}</Text>
<Checkbox
checked={selectedComponents.includes(slug)}
onChange={() => handleComponentCheckboxChange(slug)}
/>
</div>
))}
<Box marginTop="lg" marginBottom="2xs">
<Text variant="body" weight="bold">
Screen sizes
</Text>
</Box>
{screenSizes.map(({ slug, title }) => (
<div className={styles.line} key={slug}>
<Text variant="body">{title}</Text>
<Checkbox
checked={selectedScreenSizes.includes(slug)}
onChange={() => handleCheckboxChange(slug)}
/>
</div>
))}
</motion.div>
);
};
+103
View File
@@ -0,0 +1,103 @@
'use client';
import styles from './Tabs.module.css';
import { Tabs } from '@base-ui-components/react/tabs';
import { Icon } from '../../../../packages/canon/src/components/Icon';
import { Text } from '../../../../packages/canon/src/components/Text';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/navigation';
import { usePlayground } from '@/utils/playground-context';
export const TabsVersion = () => {
const { selectedThemeName, setSelectedThemeName } = usePlayground();
return (
<Tabs.Root
className={styles.tabs}
onValueChange={setSelectedThemeName}
value={selectedThemeName}
>
<Tabs.List className={styles.list}>
<Tabs.Tab className={styles.tab} value="legacy">
<Text variant="caption" weight="bold">
Theme 1
</Text>
</Tabs.Tab>
<Tabs.Tab className={styles.tab} value="default">
<Text variant="caption" weight="bold">
Theme 2
</Text>
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
export const TabsTheme = () => {
const { selectedTheme, setSelectedTheme } = usePlayground();
return (
<Tabs.Root
className={styles.tabsTheme}
onValueChange={setSelectedTheme}
value={selectedTheme}
>
<Tabs.List className={styles.list}>
<Tabs.Tab className={styles.tab} value="light">
<Icon name="sun" />
</Tabs.Tab>
<Tabs.Tab className={styles.tab} value="dark">
<Icon name="moon" />
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
export const TabsPages = () => {
const pathname = usePathname();
const router = useRouter();
const onValueChange = (value: string) => {
if (value === 'docs') {
router.push('/');
} else {
router.push('/playground');
}
};
return (
<Tabs.Root
className={styles.tabs}
value={pathname.includes('playground') ? 'playground' : 'docs'}
onValueChange={onValueChange}
>
<Tabs.List className={styles.list}>
<Tabs.Tab
className={styles.tab}
value="docs"
onClick={() => {
router.push('/');
}}
>
<Text variant="caption" weight="bold">
Documentation
</Text>
</Tabs.Tab>
<Tabs.Tab
className={styles.tab}
value="playground"
onClick={() => {
router.push('/playground');
}}
>
<Text variant="caption" weight="bold">
Playground
</Text>
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
@@ -0,0 +1,42 @@
import { ReactNode } from 'react';
import { CodeBlock } from '../CodeBlock';
import { Text } from '../../../../packages/canon/src/components/Text';
import { Collapsible } from '@base-ui-components/react/collapsible';
import styles from './styles.module.css';
interface SnippetProps {
preview: ReactNode;
code: string;
align?: 'left' | 'center';
px?: number;
py?: number;
open?: boolean;
}
export const Snippet = ({
preview,
code = '',
align = 'left',
px = 2,
py = 2,
open = false,
}: SnippetProps) => {
return (
<Collapsible.Root className={styles.container} defaultOpen={open}>
<div className={styles.preview}>
<div
className={`${styles.previewContent} ${styles[align]}`}
style={{ padding: `${py}rem ${px}rem` }}
>
{preview}
</div>
<Collapsible.Trigger className={styles.trigger}>
<Text variant="body">View code</Text>
</Collapsible.Trigger>
</div>
<Collapsible.Panel className={styles.panel}>
<CodeBlock code={code} />
</Collapsible.Panel>
</Collapsible.Root>
);
};
@@ -0,0 +1,62 @@
.container {
display: flex;
flex-direction: column;
gap: 8px;
}
.preview {
border-radius: 4px;
box-shadow: inset 0 0 0 1px var(--canon-border-base);
background-color: var(--canon-background);
transition: all 0.2s ease-in-out;
padding: 1px;
position: relative;
}
.previewContent {
width: 100%;
height: 100%;
background-image: radial-gradient(rgba(0, 0, 0, 0.08) 1px, transparent 0);
background-position: calc((8px / 2) * -1) calc((8px / 2) * -1);
background-size: 8px 8px;
}
.center {
display: flex;
justify-content: center;
align-items: center;
}
.trigger {
all: unset;
position: absolute;
right: 16px;
bottom: 12px;
cursor: pointer;
}
[data-theme='dark'] .previewContent {
background-image: radial-gradient(
rgba(255, 255, 255, 0.1) 1px,
transparent 0
);
}
.panel {
height: var(--collapsible-panel-height);
transition: all 0.2s ease-out;
overflow: hidden;
&[data-starting-style],
&[data-ending-style] {
height: 0;
}
&[data-closed] {
opacity: 0;
}
&[data-open] {
opacity: 1;
}
}
+54
View File
@@ -0,0 +1,54 @@
/*
* 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 './styles.module.css';
export const Root = ({ children }: { children: React.ReactNode }) => {
return (
<div className={styles.wrapper}>
<table className={styles.table}>{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={`${styles.tableCell} ${styles.tableHeaderCell}`}>
{children}
</th>
);
};
export const Row = ({ children }: { children: React.ReactNode }) => {
return <tr className={styles.tableRow}>{children}</tr>;
};
export const Cell = ({ children }: { children: React.ReactNode }) => {
return <td className={styles.tableCell}>{children}</td>;
};
+16
View File
@@ -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 { Root, Header, Body, Row, Cell, HeaderRow, HeaderCell } from './Table';
@@ -0,0 +1,73 @@
/*
* 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.
*/
.wrapper {
border: 1px solid var(--canon-border-base);
border-radius: 4px;
overflow: hidden;
margin-bottom: 1rem;
}
.table {
width: 100%;
margin: 0 !important;
padding: 0 !important;
border-spacing: 0px;
border-collapse: collapse;
}
.tableCell {
padding: 12px 16px !important;
border: none !important;
text-align: left;
background-color: var(--canon-surface-1) !important;
font-size: 16px;
transition: background-color 0.2s ease-in-out;
& p {
margin: 0;
}
}
.tableHeaderCell {
border-bottom: 1px solid var(--canon-border-base) !important;
font-weight: 500;
font-size: 14px;
}
.tableRow {
border: none;
border-bottom: 1px solid var(--canon-border-base);
&:last-child {
border-bottom: none;
}
}
.tableChip {
display: inline-block;
font-size: 14px !important;
border: 1px solid var(--canon-border-base);
border-radius: 6px;
padding: 0px 6px;
height: 24px;
}
.tableType {
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: 8px;
}
+1
View File
@@ -0,0 +1 @@
export * as Tabs from './parts';
+54
View File
@@ -0,0 +1,54 @@
'use client';
import { Tabs as TabsPrimitive } from '@base-ui-components/react/tabs';
import styles from './styles.module.css';
import { Text } from '../../../../packages/canon/src/components/Text';
export const Root = ({
className,
...rest
}: React.ComponentProps<typeof TabsPrimitive.Root>) => (
<TabsPrimitive.Root className={`${styles.root} ${className}`} {...rest} />
);
export const List = ({
className,
children,
...rest
}: React.ComponentProps<typeof TabsPrimitive.List>) => (
<TabsPrimitive.List className={`${styles.list} ${className}`} {...rest}>
{children}
<TabsPrimitive.Indicator className={styles.indicator} />
</TabsPrimitive.List>
);
export const Tab = (props: React.ComponentProps<typeof TabsPrimitive.Tab>) => (
<TabsPrimitive.Tab
{...props}
render={({ children, ...rest }, state) => {
return (
<button className={styles.tab} {...rest}>
<Text
variant="subtitle"
weight="bold"
{...rest}
style={{
color: state.selected
? 'var(--canon-text-primary)'
: 'var(--canon-text-secondary)',
}}
>
{children}
</Text>
</button>
);
}}
/>
);
export const Panel = ({
className,
...rest
}: React.ComponentProps<typeof TabsPrimitive.Panel>) => (
<TabsPrimitive.Panel className={`${styles.panel} ${className}`} {...rest} />
);
@@ -0,0 +1,27 @@
.root {
margin-top: 40px;
}
.list {
display: flex;
gap: var(--canon-spacing-md);
border-bottom: 1px solid var(--canon-border-base);
position: relative;
margin-bottom: var(--canon-spacing-md);
}
.tab {
all: unset;
cursor: pointer;
padding-bottom: 8px;
}
.indicator {
position: absolute;
bottom: -1px;
left: var(--active-tab-left);
width: var(--active-tab-width);
height: 1px;
background-color: var(--canon-text-primary);
transition: all 0.2s ease-in-out;
}
+54
View File
@@ -0,0 +1,54 @@
import type { MDXComponents } from 'mdx/types';
import Image, { ImageProps } from 'next/image';
import { ReactNode } from 'react';
import React from 'react';
import { CodeBlock } from '@/components/CodeBlock';
import { Heading } from '../../packages/canon/src/components/Heading';
import { Text } from '../../packages/canon/src/components/Text';
import { Box } from '../../packages/canon/src/components/Box';
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// Allows customizing built-in components, e.g. to add styling.
h1: ({ children }) => (
<Box marginBottom="md" style={{ marginTop: '4rem' }}>
<Heading variant="title2">{children as ReactNode}</Heading>
</Box>
),
h2: ({ children }) => (
<Box marginTop="2xl" marginBottom="md">
<Heading variant="title4">{children as ReactNode}</Heading>
</Box>
),
h3: ({ children }) => (
<Box marginTop="xl" marginBottom="xs">
<Heading variant="title5">{children as ReactNode}</Heading>
</Box>
),
p: ({ children }) => (
<Box marginBottom="sm">
<Text variant="subtitle">{children as ReactNode}</Text>
</Box>
),
a: ({ children, href }) => (
<a href={href} style={{ color: 'var(--canon-text-primary)' }}>
{children as ReactNode}
</a>
),
pre: ({ children }) => {
const codeContent = React.isValidElement(children)
? (children.props as { children: string }).children
: '';
return <CodeBlock lang="tsx" code={codeContent} />;
},
img: props => (
<Image
sizes="100vw"
style={{ width: '100%', height: 'auto' }}
{...(props as ImageProps)}
/>
),
...components,
};
}
+45
View File
@@ -0,0 +1,45 @@
// Sometimes codes are not formatted correctly in the docs, so we need to use snippets
export const grid = `import { Grid } from '@backstage/canon';
<Grid>
<Grid.Item />
</Grid>
`;
export const buttonVariants = `<Inline alignY="center">
<Button iconStart="cloud" variant="primary">
Button
</Button>
<Button iconStart="cloud" variant="secondary">
Button
</Button>
<Button iconStart="cloud" variant="tertiary">
Button
</Button>
</Inline>
`;
export const stackFAQ1 = `<Grid columns={3} gap="md">
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Grid>`;
export const stackSimple = `<Stack>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Stack>`;
export const stackResponsive = `<Stack gap={{ xs: 'sm', md: 'md' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Stack>`;
export const stackAlign = `<Stack align={{ xs: 'left', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Stack>`;
+20
View File
@@ -0,0 +1,20 @@
'use client';
import { Box } from '../../../packages/canon';
export const BoxPreview = () => {
return (
<Box
style={{
width: '64px',
height: '64px',
background: '#eaf2fd',
borderRadius: '4px',
border: '1px solid #2563eb',
color: '#2563eb',
backgroundImage:
'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")',
}}
/>
);
};
+108
View File
@@ -0,0 +1,108 @@
'use client';
import {
Inline,
Button,
Stack,
ButtonProps,
Text,
} from '../../../packages/canon';
export const ButtonPreview = () => {
return (
<Inline alignY="center">
<Button iconStart="cloud" variant="primary">
Button
</Button>
<Button iconStart="cloud" variant="secondary">
Button
</Button>
<Button iconStart="cloud" variant="tertiary">
Button
</Button>
</Inline>
);
};
export const ButtonSizes = () => {
return (
<Inline alignY="center">
<Button size="medium">Medium</Button>
<Button size="small">Small</Button>
</Inline>
);
};
export const ButtonWithIcons = () => {
return (
<Inline alignY="center">
<Button iconStart="cloud">Button</Button>
<Button iconEnd="chevronRight">Button</Button>
<Button iconStart="cloud" iconEnd="chevronRight">
Button
</Button>
</Inline>
);
};
export const ButtonFullWidth = () => {
return (
<Stack style={{ width: '300px' }}>
<Button iconStart="cloud">Button</Button>
<Button iconEnd="chevronRight">Button</Button>
<Button iconStart="cloud" iconEnd="chevronRight">
Button
</Button>
</Stack>
);
};
export const ButtonDisabled = () => {
return <Button disabled>Button</Button>;
};
export const ButtonResponsive = () => {
// TODO: Add responsive button
return null;
};
export const ButtonPlayground = () => {
const variants: string[] = ['primary', 'secondary', 'tertiary'];
return (
<Stack>
{variants.map(variant => (
<Stack key={variant}>
<Text>{variant}</Text>
{['small', 'medium'].map(size => (
<Inline alignY="center" key={size}>
<Button
iconStart="cloud"
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
>
Button
</Button>
<Button
iconEnd="chevronRight"
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
>
Button
</Button>
<Button
iconStart="cloud"
iconEnd="chevronRight"
style={{ width: '200px' }}
variant={variant as ButtonProps['variant']}
size={size as ButtonProps['size']}
>
Button
</Button>
</Inline>
))}
</Stack>
))}
</Stack>
);
};
+32
View File
@@ -0,0 +1,32 @@
'use client';
import { Inline, Checkbox, Stack, Text } from '../../../packages/canon';
export const CheckboxPreview = () => {
return <Checkbox label="Accept terms and conditions" />;
};
export const CheckboxAllVariants = () => {
return (
<Inline alignY="center">
<Checkbox />
<Checkbox checked />
<Checkbox label="Checkbox" />
<Checkbox label="Checkbox" checked />
</Inline>
);
};
export const CheckboxPlayground = () => {
return (
<Stack>
<Text>All variants</Text>
<Inline alignY="center">
<Checkbox />
<Checkbox checked />
<Checkbox label="Checkbox" />
<Checkbox label="Checkbox" checked />
</Inline>
</Stack>
);
};
+25
View File
@@ -0,0 +1,25 @@
'use client';
import { Box, Container } from '../../../packages/canon';
const DecorativeBox = () => (
<Box
borderRadius="xs"
style={{
height: '64px',
background: '#eaf2fd',
borderRadius: '4px',
border: '1px solid #2563eb',
backgroundImage:
'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")',
}}
/>
);
export const ContainerPreview = () => {
return (
<Container>
<DecorativeBox />
</Container>
);
};
+27
View File
@@ -0,0 +1,27 @@
'use client';
import { Box, Grid } from '../../../packages/canon';
const FakeBox = () => (
<Box
borderRadius="xs"
style={{
background: '#eaf2fd',
borderRadius: '4px',
boxShadow: '0 0 0 1px #2563eb',
height: '64px',
backgroundImage:
'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")',
}}
/>
);
export const GridPreview = () => {
return (
<Grid>
<FakeBox />
<FakeBox />
<FakeBox />
</Grid>
);
};
+43
View File
@@ -0,0 +1,43 @@
'use client';
import { Heading, Stack, Text } from '../../../packages/canon';
export const HeadingPreview = () => {
return <Heading>Look mum, no hands!</Heading>;
};
export const HeadingAllVariants = () => {
return (
<Stack>
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Stack>
);
};
export const HeadingResponsive = () => {
return null;
return (
<Stack>
<Heading variant={{ xs: 'title4', md: 'title2' }}>
Responsive heading
</Heading>
</Stack>
);
};
export const HeadingPlayground = () => {
return (
<Stack>
<Text>All variants</Text>
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Stack>
);
};
+7
View File
@@ -0,0 +1,7 @@
'use client';
import { Icon } from '../../../packages/canon';
export const IconPreview = () => {
return <Icon name="heart" />;
};
+48
View File
@@ -0,0 +1,48 @@
'use client';
import { Box, Inline } from '../../../packages/canon';
const fakeBlockList = [
{ width: 45, height: 60 },
{ width: 150, height: 75 },
{ width: 80, height: 50 },
{ width: 120, height: 70 },
{ width: 95, height: 65 },
{ width: 80, height: 32 },
{ width: 130, height: 60 },
{ width: 100, height: 80 },
{ width: 140, height: 45 },
{ width: 85, height: 70 },
{ width: 125, height: 50 },
];
const FakeBox = ({
width = 120,
height = 80,
}: {
width?: number;
height?: number;
}) => (
<Box
borderRadius="xs"
style={{
background: '#eaf2fd',
borderRadius: '4px',
boxShadow: '0 0 0 1px #2563eb',
width,
height,
backgroundImage:
'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")',
}}
/>
);
export const InlinePreview = () => {
return (
<Inline>
{fakeBlockList.map((block, index) => (
<FakeBox key={index} width={block.width} height={block.height} />
))}
</Inline>
);
};
+31
View File
@@ -0,0 +1,31 @@
'use client';
import { Stack } from '../../../packages/canon';
const DecorativeBox = () => {
return (
<div
style={{
background: '#eaf2fd',
borderRadius: '4px',
boxShadow: '0 0 0 1px #2563eb',
height: '32px',
minWidth: '100px',
backgroundImage:
'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")',
}}
/>
);
};
export const StackPreview = () => {
return (
<div style={{ width: '320px' }}>
<Stack>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Stack>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More