Move canon-docs to docs-ui

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-07-15 11:09:59 +01:00
parent 4c01d54643
commit 5dde3bfe92
155 changed files with 46 additions and 46 deletions
+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"
}
}
+9
View File
@@ -0,0 +1,9 @@
# next.js
/.next/
/dist/
next-env.d.ts
# css
/public/core.css
/public/components.css
/public/backstage.css
+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.
+20
View File
@@ -0,0 +1,20 @@
import createMDX from '@next/mdx';
const nextConfig = {
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
output: 'export',
assetPrefix: '/',
distDir: 'dist',
images: {
unoptimized: true,
},
typescript: {
// Ignore TypeScript errors during build - safe for React 18/19 compatibility issues
// These are type-level conflicts that don't affect runtime behavior
ignoreBuildErrors: true,
},
};
const withMDX = createMDX({});
export default withMDX(nextConfig);
+51
View File
@@ -0,0 +1,51 @@
{
"name": "canon-docs",
"version": "0.1.0",
"private": true,
"scripts": {
"prebuild": "yarn sync:css",
"build": "next build",
"lint": "next lint",
"prestart": "yarn sync:css",
"start": "concurrently \"yarn sync:css:watch\" \"next dev\"",
"sync:css": "node scripts/sync-css.js",
"sync:css:watch": "node scripts/sync-css.js --watch"
},
"resolutions": {
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6"
},
"dependencies": {
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/view": "^6.34.4",
"@lezer/highlight": "^1.2.1",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "15.3.4",
"@storybook/react": "^8.6.8",
"@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7",
"html-react-parser": "^5.2.5",
"motion": "^12.4.1",
"next": "15.3.4",
"next-mdx-remote-client": "^2.1.2",
"prop-types": "^15.8.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-frame-component": "^5.2.7",
"shiki": "^1.26.1",
"storybook": "^8.6.8"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
"@types/node": "^20",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"chokidar": "^3.6.0",
"concurrently": "^8.2.2",
"eslint": "^8",
"eslint-config-next": "15.3.4",
"lightningcss": "^1.28.2",
"typescript": "^5"
}
}
View File
+1
View File
@@ -0,0 +1 @@
ui.backstage.io
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+149
View File
@@ -0,0 +1,149 @@
const fs = require('fs');
const path = require('path');
const { bundle } = require('lightningcss');
const chokidar = require('chokidar');
// Configuration
const config = {
UIPath: '../../packages/ui',
publicPath: '../public',
files: [
{
source: 'css/styles.css',
destination: 'theme-backstage.css',
name: 'Main Styles',
},
{
source: '.storybook/themes/spotify.css',
destination: 'theme-spotify.css',
name: 'Spotify Theme',
},
],
};
class CSSSync {
constructor() {
this.UIPath = path.resolve(__dirname, config.UIPath);
this.publicPath = path.resolve(__dirname, config.publicPath);
this.isWatching = process.argv.includes('--watch');
}
async syncFile(fileConfig) {
const sourcePath = path.join(this.UIPath, fileConfig.source);
const destPath = path.join(this.publicPath, fileConfig.destination);
try {
// Check if source file exists
if (!fs.existsSync(sourcePath)) {
console.warn(`⚠️ Source file not found: ${sourcePath}`);
return false;
}
// Ensure destination directory exists
fs.mkdirSync(path.dirname(destPath), { recursive: true });
// Bundle and optimize CSS
const result = await bundle({
filename: sourcePath,
minify: true,
});
// Write to destination
fs.writeFileSync(destPath, result.code);
console.log(
`${fileConfig.name}: ${fileConfig.source}${fileConfig.destination}`,
);
return true;
} catch (error) {
console.error(`❌ Error syncing ${fileConfig.name}:`, error.message);
return false;
}
}
async syncAll() {
console.log('🔄 Syncing CSS files...\n');
let successCount = 0;
for (const fileConfig of config.files) {
if (await this.syncFile(fileConfig)) {
successCount++;
}
}
console.log(
`\n✨ Synced ${successCount}/${config.files.length} CSS files successfully!`,
);
if (successCount > 0) {
console.log('\n📁 Available CSS files in public/:');
config.files.forEach(file => {
const destPath = path.join(this.publicPath, file.destination);
if (fs.existsSync(destPath)) {
const stats = fs.statSync(destPath);
const size = (stats.size / 1024).toFixed(2);
console.log(`${file.destination} (${size} KB)`);
}
});
}
}
startWatching() {
console.log('👀 Watching for CSS changes...\n');
// Watch all source files
const watchPaths = config.files.map(file =>
path.join(this.UIPath, file.source),
);
const watcher = chokidar.watch(watchPaths, {
ignored: /node_modules/,
persistent: true,
});
watcher.on('change', async filePath => {
console.log(
`\n🔄 Change detected: ${path.relative(this.UIPath, filePath)}`,
);
// Find which file changed and sync it
const fileConfig = config.files.find(file =>
filePath.endsWith(file.source.replace(/\//g, path.sep)),
);
if (fileConfig) {
await this.syncFile(fileConfig);
}
});
watcher.on('error', error => console.error('❌ Watch error:', error));
// Handle process termination
process.on('SIGINT', () => {
console.log('\n👋 Stopping CSS sync...');
watcher.close();
process.exit(0);
});
}
async run() {
console.log('🎨 BUI CSS Sync Tool\n');
console.log(`📂 BUI path: ${this.UIPath}`);
console.log(`📂 Public path: ${this.publicPath}\n`);
// Initial sync
await this.syncAll();
// Watch for changes if requested
if (this.isWatching) {
this.startWatching();
}
}
}
// Run the sync tool
const cssSync = new CSSSync();
cssSync.run().catch(error => {
console.error('❌ CSS Sync failed:', error);
process.exit(1);
});
+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&apos;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&apos;s Backstage team, leveraging Spotify&apos;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&apos;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.
+13
View File
@@ -0,0 +1,13 @@
import { Changelog } from '@/components/Changelog';
# Changelog
<Changelog />
## Version 0.1.0
We're excited to share the initial release of Canon 💚 In this first alpha version,
you'll find the foundation of our design system: a set of versatile layout components
and a handful of essential atomic elements to help you get started. While Canon is
still in its early stages, it's ready for exploration and we'd love for you to give
it a try and share your feedback.
@@ -0,0 +1,25 @@
import { components, layoutComponents } from '@/utils/data';
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const { default: Component } = await import(
`@/content/components/${slug}.mdx`
);
return <Component />;
}
export function generateStaticParams() {
const list = [...components, ...layoutComponents];
return list.map(component => ({
slug: component.slug,
}));
}
export const dynamicParams = false;
+6
View File
@@ -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>;
}
+51
View File
@@ -0,0 +1,51 @@
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={`yarn add @backstage/canon`} />
### 2. Import the css files
Import the global CSS file at the root of your application.
```tsx
import '@backstage/canon/css/styles.css';
```
### 3. Start building ✨
Now you can start building your plugin using the new design system.
```tsx
import { Flex, Button, Text } from '@backstage/canon';
<Flex>
<Text>Hello World</Text>
<Button>Click me</Button>
</Flex>;
```
## 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,24 @@
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 code={`<Icon name="heart" />`} />
## Icon overrides
You can override any icons in our library by using the `IconProvider` at the root of your application.
<CodeBlock
code={`<IconProvider overrides={{ heart: () => <div>Custom Icon</div> }} />`}
/>
## 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={`<Flex direction="column" gap="4">
<Box>Hello World</Box>
<Inline gap="sm">
<Box>Project 1</Box>
<Box>Project 2</Box>
</Inline>
</Flex>
`}
/>
<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,107 @@
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>`} />
@@ -0,0 +1,693 @@
import { CodeBlock } from '@/components/CodeBlock';
import * as Table from '@/components/Table';
import { Chip } from '@/components/Chip';
import { customTheme } from '@/snippets/code-snippets';
# Theming
Canon's theming is built entirely on CSS, without relying on any CSS-in-JS libraries.
At its core, it provides a solid default theme that is easily customizable using a
comprehensive set of CSS variables. Additionally, it enables anyone to adapt the design
to their specific needs. Each component comes with fixed class names, making customization
even more straightforward.
## Light & Dark modes
By default, Canon supports both light and dark modes using the `data-theme` attribute.
The light theme is applied by default if no `data-theme` attribute is specified. To create
a custom theme, you'll need to define both light and dark modes as outlined below. If
only one mode is defined, the other will fall back to the default theme.
## How to create your own theme
In our [started guide](/), we ask you to import two css files. The `core.css` file includes
the default set of variables. We recommend to keep this file in place and add your own theme
on top of it. `core.css` also include an opinionated reset. If you decided to remove `core.css`
you will have to provide your own reset css.
Here's an example of how your theme.css file should look like:
<CodeBlock lang="css" code={customTheme} />
## CSS class name structure
All Canon components come with a set of CSS classes that you can use to style them. To make it
easier to identify the class name you can use, we use a specific structure for the class names.
<img
src="/css-classname-structure.png"
style={{ width: '100%', marginBottom: '32px', marginTop: '16px' }}
/>
Every component has a unique prefix `.canon-` followed by the component name. Component props
are represented using the `data-` attribute. That way, class names are easily identifiable.
## Available CSS variables
### Base colors
These colors are used for special purposes like ring, scrollbar, ...
<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-black</Chip>
</Table.Cell>
<Table.Cell>
Pure black color. This one should be the same in light and dark themes.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-white</Chip>
</Table.Cell>
<Table.Cell>
Pure white color. This one should be the same in light and dark themes.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-gray-1</Chip>
</Table.Cell>
<Table.Cell>You can use these mostly for backgrounds colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-gray-2</Chip>
</Table.Cell>
<Table.Cell>You can use these mostly for backgrounds colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-gray-3</Chip>
</Table.Cell>
<Table.Cell>You can use these mostly for backgrounds colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-gray-4</Chip>
</Table.Cell>
<Table.Cell>You can use these mostly for backgrounds colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-gray-5</Chip>
</Table.Cell>
<Table.Cell>You can use these mostly for backgrounds colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-gray-6</Chip>
</Table.Cell>
<Table.Cell>You can use these mostly for backgrounds colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-gray-7</Chip>
</Table.Cell>
<Table.Cell>You can use these mostly for backgrounds colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-gray-8</Chip>
</Table.Cell>
<Table.Cell>You can use these mostly for backgrounds colors.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
### Core background colors
These colors are used for the background of your application. We are mostly using for now a
single elevated background for panels. `--canon-bg` should mostly use as the main background
color of your app.
<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-bg</Chip>
</Table.Cell>
<Table.Cell>The background color of your Backstage instance.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-surface-1</Chip>
</Table.Cell>
<Table.Cell>Use for any panels or elevated surfaces.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-surface-2</Chip>
</Table.Cell>
<Table.Cell>Use for any panels or elevated surfaces.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-solid</Chip>
</Table.Cell>
<Table.Cell>Used for solid background colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-solid-hover</Chip>
</Table.Cell>
<Table.Cell>Used for solid background colors when hovered.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-solid-pressed</Chip>
</Table.Cell>
<Table.Cell>Used for solid background colors when pressed.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-solid-disabled</Chip>
</Table.Cell>
<Table.Cell>Used for solid background colors when disabled.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-tint</Chip>
</Table.Cell>
<Table.Cell>Used for tint background colors.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-tint-hover</Chip>
</Table.Cell>
<Table.Cell>Used for tint background colors when hovered.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-tint-focus</Chip>
</Table.Cell>
<Table.Cell>Used for tint background colors when active.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-tint-disabled</Chip>
</Table.Cell>
<Table.Cell>Used for tint background colors when disabled.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-danger</Chip>
</Table.Cell>
<Table.Cell>Used to show errors information.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-warning</Chip>
</Table.Cell>
<Table.Cell>Used to show warnings information.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-bg-success</Chip>
</Table.Cell>
<Table.Cell>Used to show success information.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
### Foreground colors
Foreground colours are meant to work in pair with a background colours. Typeically this would work
for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors
are prefixed with `fg` to make it easier to identify.
<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-fg-primary</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of main background surfaces.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-secondary</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of main background surfaces.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-link</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of main background surfaces.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-link-hover</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of main background surfaces.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-disabled</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of main background surfaces.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-solid</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of solid background colors.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-tint</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of tint background colors.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-tint-disabled</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of tint background colors when disabled.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-danger</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of danger background colors.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-warning</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of warning background colors.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-fg-success</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of success background colors.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
### Border colors
These border colors are mostly meant to be used as borders on top of any components with
low contrast to help as a separator with the different background colors.
<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-border</Chip>
</Table.Cell>
<Table.Cell>
It should be used on top of `--canon-bg-surface-1`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-border-hover</Chip>
</Table.Cell>
<Table.Cell>
Used when the component is interactive and hovered.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-border-pressed</Chip>
</Table.Cell>
<Table.Cell>
Used when the component is interactive and focused.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-border-disabled</Chip>
</Table.Cell>
<Table.Cell>Used when the component is disabled.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-border-danger</Chip>
</Table.Cell>
<Table.Cell>It should be used on top of `--canon-bg-danger`.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-border-warning</Chip>
</Table.Cell>
<Table.Cell>It should be used on top of `--canon-bg-warning`.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-border-success</Chip>
</Table.Cell>
<Table.Cell>It should be used on top of `--canon-bg-success`.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
### Special colors
These colors are used for special purposes like ring, scrollbar, ...
<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-ring</Chip>
</Table.Cell>
<Table.Cell>The color of the ring.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-scrollbar</Chip>
</Table.Cell>
<Table.Cell>The color of the scrollbar.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-scrollbar-thumb</Chip>
</Table.Cell>
<Table.Cell>The color of the scrollbar thumb.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
### Font families
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>
### Font weights
We have two font weights that we use across Canon. Regular or Bold.
<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-weight-regular</Chip>
</Table.Cell>
<Table.Cell>The regular font weight for the theme.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-font-weight-bold</Chip>
</Table.Cell>
<Table.Cell>The bold font weight for the theme.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
### Spacing
We built a spacing system based on a single value `--canon-space`. This value is
used to calculate the spacing for all the components. By default if you would like to
increase or decrease the spacing between your components you can do it simply by updating
`--canon-space` and it will apply to all spacing values.
`--canon-space` is not used directly in any components but serve as an easy way to
calculate the other values.
<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</Chip>
</Table.Cell>
<Table.Cell>
The base unit for the spacing system. Default value is `0.25rem`.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
Below is the list of all spacing values you can use in your application. We use these
tokens for pretty much each spacing properties like padding, margin, gaps, ...
<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-0_5</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 0.5.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-1</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`).</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-1_5</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 1.5.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-2</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 2.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-3</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 3.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-4</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 4.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-5</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 5.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-6</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 6.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-7</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 7.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-8</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 8.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-9</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 9.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-10</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 10.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-11</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 11.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-12</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 12.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-13</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 13.</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-space-14</Chip>
</Table.Cell>
<Table.Cell>Base unit (`--canon-space`) times 14.</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
### Radius
We use a radius system to make sure that the components have a consistent look and feel.
<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-radius-1</Chip>
</Table.Cell>
<Table.Cell>
The radius of the component. Default value is `0.125rem`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-radius-2</Chip>
</Table.Cell>
<Table.Cell>
The radius of the component. Default value is `0.25rem`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-radius-3</Chip>
</Table.Cell>
<Table.Cell>
The radius of the component. Default value is `0.5rem`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-radius-4</Chip>
</Table.Cell>
<Table.Cell>
The radius of the component. Default value is `0.75rem`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-radius-5</Chip>
</Table.Cell>
<Table.Cell>
The radius of the component. Default value is `1rem`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-radius-6</Chip>
</Table.Cell>
<Table.Cell>
The radius of the component. Default value is `1.25rem`.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>--canon-radius-full</Chip>
</Table.Cell>
<Table.Cell>
The radius of the component. Default value is `9999px`.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
@@ -0,0 +1,65 @@
import { HeadingSnippet, TextSnippet } from '@/snippets/stories-snippets';
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={<HeadingSnippet story="AllVariants" />}
code={`<Flex direction="column" gap="4">
<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>
</Flex>`}
/>
## 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={<TextSnippet story="AllVariants" />}
code={`<Flex direction="column" gap="4">
<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>
</Flex>`}
/>
@@ -0,0 +1,87 @@
'use client';
import { ReactNode } from 'react';
import { Grid, Flex, Text } from '../../../../../packages/canon';
import { screenSizes } from '@/utils/data';
import { Frame } from '@/components/Frame';
import { usePlayground } from '@/utils/playground-context';
import {
ButtonSnippet,
CheckboxSnippet,
HeadingSnippet,
TextSnippet,
} from '@/snippets/stories-snippets';
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 (
<Flex direction="column" gap="4">
{selectedComponents.find(c => c === 'button') && (
<Line content={<ButtonSnippet story="Playground" />} title="Button" />
)}
{selectedComponents.find(c => c === 'checkbox') && (
<Line
content={<CheckboxSnippet story="Playground" />}
title="Checkbox"
/>
)}
{selectedComponents.find(c => c === 'heading') && (
<Line content={<HeadingSnippet story="Playground" />} title="Heading" />
)}
{selectedComponents.find(c => c === 'text') && (
<Line content={<TextSnippet story="Playground" />} title="Text" />
)}
</Flex>
);
};
const Line = ({ content, title }: { content: ReactNode; title: string }) => {
return (
<Grid.Root gap={{ xs: '2', md: '4' }}>
<Grid.Item colSpan="2">
<Text>{title}</Text>
</Grid.Item>
<Grid.Item colSpan="10">{content}</Grid.Item>
</Grid.Root>
);
};
@@ -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(--border);
background-color: var(--bg);
padding: 16px;
}
+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

+44
View File
@@ -0,0 +1,44 @@
import type { Metadata } from 'next';
import { Sidebar } from '../components/Sidebar';
import { Toolbar } from '@/components/Toolbar';
import { Providers } from './providers';
import { CustomTheme } from '@/components/CustomTheme';
import styles from '../css/page.module.css';
import '../css/globals.css';
import '/public/theme-backstage.css';
import '/public/theme-spotify.css';
export const metadata: Metadata = {
title: 'Canon',
description: 'UI library for Backstage',
metadataBase: new URL('https://canon.backstage.io'),
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
data-theme="light"
data-theme-name="default"
suppressHydrationWarning
>
<body>
<Providers>
<div className={styles.global}>
<Sidebar />
<div className={styles.container}>
<Toolbar />
{children}
</div>
<CustomTheme />
</div>
</Providers>
</body>
</html>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

+8
View File
@@ -0,0 +1,8 @@
'use client';
import { ReactNode } from 'react';
import { PlaygroundProvider } from '@/utils/playground-context';
export const Providers = ({ children }: { children: ReactNode }) => {
return <PlaygroundProvider>{children}</PlaygroundProvider>;
};
+26
View File
@@ -0,0 +1,26 @@
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>
);
};
+1
View File
@@ -0,0 +1 @@
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;
}
@@ -0,0 +1,43 @@
import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
export function Changelog() {
// Group changelog entries by version
const groupedChangelog = changelog.reduce((acc, entry) => {
if (!acc[entry.version]) {
acc[entry.version] = [];
}
acc[entry.version].push(entry);
return acc;
}, {} as Record<string, typeof changelog>);
// Sort versions in descending order
const sortedVersions = Object.keys(groupedChangelog).sort((a, b) =>
b.localeCompare(a),
);
const content = sortedVersions
.map(version => {
const entries = groupedChangelog[version];
return `## Version ${version}
${entries
.map(e => {
const prs =
e.prs.length > 0 &&
e.prs
.map(
pr =>
`[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
)
.join(', ');
return `- ${e.description} ${prs}`;
})
.join('\n')}`;
})
.join('\n');
return <MDXRemote components={formattedMDXComponents} source={content} />;
}
+16
View File
@@ -0,0 +1,16 @@
import { ReactNode } from 'react';
import styles from './styles.module.css';
export const Chip = ({
children,
head = false,
}: {
children: ReactNode;
head?: boolean;
}) => {
return (
<span className={`${styles.chip} ${head ? styles.head : ''}`}>
{children}
</span>
);
};
+1
View File
@@ -0,0 +1 @@
export { Chip } from './Chip';
@@ -0,0 +1,26 @@
.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;
}
.head {
background-color: #eaf2fd;
color: #2563eb;
}
[data-theme='dark'] .chip {
background-color: #2c2c2c;
color: #fff;
}
[data-theme='dark'] .chip.head {
background-color: #33405b;
}
@@ -0,0 +1,25 @@
'use client';
import { CodeBlockProps } from '.';
import { Text } from '@backstage/canon';
import styles from './styles.module.css';
import parse from 'html-react-parser';
export const CodeBlockClient = ({
out,
title,
}: {
out: string;
title?: CodeBlockProps['title'];
}) => {
return (
<div className={styles.codeBlock}>
{title && (
<div className={styles.title}>
<Text variant="body">{title}</Text>
</div>
)}
<div className={styles.code}>{parse(out)}</div>
</div>
);
};
@@ -0,0 +1,21 @@
import type { BundledLanguage } from 'shiki';
import { codeToHtml } from 'shiki';
import { CodeBlockClient } from './client';
export 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 <CodeBlockClient out={out} title={title} />;
}
@@ -0,0 +1,25 @@
.codeBlock {
border-radius: 4px;
border: 1px solid var(--border);
position: relative;
background: transparent;
overflow-x: auto;
font-family: var(--font-mono);
background-color: #fff;
margin-bottom: 1rem;
}
[data-theme='dark'] .codeBlock {
background-color: #121212;
}
.title {
border-bottom: 1px solid var(--border);
padding: 12px 20px;
font-size: 0.875rem;
color: var(--secondary);
}
.code {
padding: 20px;
}
@@ -0,0 +1,15 @@
import { ReactNode, CSSProperties } from 'react';
export const Columns = ({
children,
style,
}: {
children: ReactNode;
style?: CSSProperties;
}) => {
return (
<div className="columns" style={style}>
{children}
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export { Columns } from './Columns';
@@ -0,0 +1,6 @@
.columns {
display: grid;
grid-template-columns: repeat(3, 1fr);
row-gap: 20px;
column-gap: 80px;
}
@@ -0,0 +1,68 @@
import { Tabs } from '@/components/Tabs';
import { CodeBlock } from '@/components/CodeBlock';
import type { ComponentInfosProps } from './types';
import styles from '@/css/mdx.module.css';
import Link from 'next/link';
import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
export const ComponentInfos = ({
usageCode,
component,
classNames,
}: ComponentInfosProps) => {
const componentChangelog = changelog.filter(c =>
c.components.includes(component),
);
return (
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Usage</Tabs.Tab>
{classNames && classNames.length > 0 && <Tabs.Tab>Theming</Tabs.Tab>}
<Tabs.Tab>Changelog</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>
<CodeBlock code={usageCode} />
</Tabs.Panel>
{classNames && classNames.length > 0 && (
<Tabs.Panel>
<p className={styles.p}>
We recommend starting with our{' '}
<Link className={styles.a} href="/theme/theming">
global tokens
</Link>{' '}
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.
</p>
<MDXRemote
components={formattedMDXComponents}
source={`${classNames
?.map(className => `- \`${className}\``)
.join('\n')}`}
/>
</Tabs.Panel>
)}
<Tabs.Panel>
<MDXRemote
components={formattedMDXComponents}
source={`${componentChangelog
?.map(change => {
const prs =
change.prs.length > 0 &&
change.prs
.map(
pr =>
`[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
)
.join(', ');
return `- \`${change.version}\` - ${change.description} ${prs}`;
})
.join('\n')}`}
/>
</Tabs.Panel>
</Tabs.Root>
);
};
@@ -0,0 +1 @@
export { ComponentInfos } from './ComponentInfos';
@@ -0,0 +1,9 @@
import type { Component } from '@/utils/changelog';
import { ReactNode } from 'react';
export interface ComponentInfosProps {
usageCode?: string;
classNames?: string[];
component: Component;
children: ReactNode;
}
@@ -0,0 +1,147 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { sass } from '@codemirror/lang-sass';
import styles from './styles.module.css';
import { usePlayground } from '@/utils/playground-context';
import { AnimatePresence, motion } from 'motion/react';
import { Icon } from '../../../../packages/canon';
import { createTheme } from '@uiw/codemirror-themes';
import { tags as t } from '@lezer/highlight';
const defaultTheme = `:root {
--canon-bg-solid: #000;
}`;
const myTheme = createTheme({
theme: 'light',
settings: {
background: 'var(--surface-1)',
backgroundImage: '',
foreground: '#6182B8',
caret: '#5d00ff',
selection: '#036dd626',
selectionMatch: '#036dd626',
lineHighlight: '#8a91991a',
gutterBackground: '#fff',
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.comment, color: '#787b8099' },
{ tag: t.variableName, color: '#0080ff' },
{ tag: [t.string, t.special(t.brace)], color: '#6182B8' },
{ tag: t.number, color: '#6182B8' },
{ tag: t.bool, color: '#6182B8' },
{ tag: t.null, color: '#6182B8' },
{ tag: t.keyword, color: '#6182B8' },
{ tag: t.operator, color: '#6182B8' },
{ tag: t.className, color: '#6182B8' },
{ tag: t.definition(t.typeName), color: '#6182B8' },
{ tag: t.typeName, color: '#6182B8' },
{ tag: t.angleBracket, color: '#6182B8' },
{ tag: t.tagName, color: '#6182B8' },
{ tag: t.attributeName, color: '#6182B8' },
],
});
export const CustomTheme = () => {
const [isClient, setIsClient] = useState(false);
const [open, setOpen] = useState(true);
const [customTheme, setCustomTheme] = useState<string | undefined>(undefined);
const { selectedThemeName } = usePlayground();
const [savedMessage, setSavedMessage] = useState<string>('Save');
const updateStyleElement = (theme: string) => {
let styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'custom-theme-style';
document.head.appendChild(styleElement);
}
styleElement.textContent = theme;
};
useEffect(() => {
if (selectedThemeName === 'custom') {
let storedTheme = localStorage.getItem('customThemeCss');
if (!storedTheme) {
storedTheme = defaultTheme;
localStorage.setItem('customThemeCss', storedTheme);
}
setCustomTheme(storedTheme);
updateStyleElement(storedTheme);
} else {
const styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
if (styleElement) {
styleElement.remove();
}
}
}, [selectedThemeName]);
useEffect(() => {
setIsClient(true);
}, []);
const handleSave = () => {
if (customTheme) {
localStorage.setItem('customThemeCss', customTheme);
updateStyleElement(customTheme);
setSavedMessage('Saved!');
setTimeout(() => setSavedMessage('Save'), 1000);
}
};
const handleChange = useCallback((val: string) => {
setCustomTheme(val);
}, []);
if (isClient === false) return null;
return (
<AnimatePresence>
{selectedThemeName === 'custom' && (
<motion.div
className={`${styles.container} ${open ? styles.open : ''}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
<div className={styles.header}>
<div className={styles.headerLeft}>Custom Theme</div>
<div className={styles.headerRight}>
{open && (
<button className={styles.buttonSave} onClick={handleSave}>
{savedMessage}
</button>
)}
<button
className={styles.buttonClose}
onClick={() => setOpen(!open)}
>
<Icon name={open ? 'chevron-down' : 'chevron-up'} />
</button>
</div>
</div>
<div className={styles.editorContainer}>
<CodeMirror
value={customTheme}
height="300px"
extensions={[sass()]}
onChange={handleChange}
className={styles.editor}
basicSetup={{ foldGutter: false }}
theme={myTheme}
/>
</div>
</motion.div>
)}
</AnimatePresence>
);
};
@@ -0,0 +1 @@
export { CustomTheme } from './customTheme';
@@ -0,0 +1,77 @@
.container {
position: fixed;
bottom: 16px;
right: 16px;
width: 240px;
height: 47px;
background-color: var(--panel);
border-radius: 0.375rem;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
transition-property: background-color, border-color, height, width;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
}
.open {
width: 36%;
height: 348px;
}
.editor {
flex: 1;
}
.editorContainer {
overflow: hidden;
}
.header {
height: 46px;
flex-shrink: 0;
border-bottom: 1px solid var(--border);
background-color: var(--panel);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px 0 16px;
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
}
.headerLeft {
font-size: 0.875rem;
}
.headerRight {
display: flex;
gap: 8px;
}
.buttonSave {
all: unset;
height: 28px;
padding: 0 8px;
color: #fff;
background-color: #000;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.75rem;
}
.buttonClose {
all: unset;
height: 28px;
padding: 0 8px;
color: #fff;
background-color: var(--bg);
color: var(--primary);
transition: background-color 0.2s ease-in-out;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
display: flex;
align-items: center;
justify-content: center;
}
@@ -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");
}
+32
View File
@@ -0,0 +1,32 @@
'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,44 @@
'use client';
import { Icon, Text } from '../../../../packages/canon';
import styles from './styles.module.css';
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="external-link" />
</a>
)}
</div>
</div>
);
};
@@ -0,0 +1,42 @@
.container {
display: flex;
background-color: var(--panel);
padding: 1.5rem;
border-radius: 0.25rem;
border: 1px solid var(--border);
margin-bottom: 1.5rem;
gap: 1.5rem;
transition: background-color 0.2s ease-in-out;
}
.icon path {
fill: var(--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(--bg);
border: 1px solid var(--border);
border-radius: 0.25rem;
padding: 0 0.75rem;
height: 28px;
border-radius: 100px;
margin-top: 0.75rem;
font-size: 0.875rem;
color: var(--primary);
gap: 0.5rem;
}
@@ -0,0 +1,22 @@
'use client';
import { Text, Icon, icons } from '../../../../packages/canon';
import type { IconNames } from '../../../../packages/canon';
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 @@
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-radius: 0.5rem;
border: 1px solid var(--border);
background-color: var(--panel);
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: var(--surface-1);
}
}
@@ -0,0 +1,48 @@
.layoutComponents {
display: flex;
justify-content: flex-start;
gap: 1rem;
flex-wrap: wrap;
margin-top: 2rem;
& svg rect {
transition: fill 0.2s ease-in-out;
}
& .box {
display: flex;
flex-direction: column;
width: calc(50% - 0.5rem);
margin-bottom: 1rem;
align-items: flex-start;
}
& .content {
flex: none;
background-color: var(--panel);
border: 1px solid var(--border);
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: var(--secondary);
}
}
@@ -0,0 +1,49 @@
import { BoxSvg } from './svgs/box';
import { FlexSvg } from './svgs/flex';
import { GridSvg } from './svgs/grid';
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/flex">
<FlexSvg />
</Link>
<div className={styles.title}>Flex</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/container">
<ContainerSvg />
</Link>
<div className={styles.title}>Container</div>
<div className={styles.description}>
A container for your components
</div>
</div>
</div>
);
};
@@ -0,0 +1 @@
export { LayoutComponents } from './LayoutComponents';
@@ -0,0 +1,37 @@
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="var(--surface-1)" />
<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,354 @@
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="var(--surface-1)"
/>
<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,51 @@
export const FlexSvg = () => {
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="var(--surface-1)" />
<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="var(--surface-1)" />
<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="var(--surface-1)" />
</svg>
);
};
@@ -0,0 +1,65 @@
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="var(--surface-1)" />
<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="var(--surface-1)"
/>
<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="var(--surface-1)"
/>
</svg>
);
};
@@ -0,0 +1,82 @@
'use client';
import * as Table from '../Table';
import { Chip } from '../Chip';
import { icons } from '../../../../packages/canon';
// Define a more specific type for the data object
type PropData = {
values?: string | string[];
responsive?: boolean;
default?: string;
type?: string;
};
// Modify the PropsTable component to use the new type
export const PropsTable = <T extends Record<string, PropData>>({
data,
}: {
data: T;
}) => {
if (!data) return null;
return (
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell style={{ width: '16%' }}>Prop</Table.HeaderCell>
<Table.HeaderCell style={{ width: '50%' }}>Type</Table.HeaderCell>
<Table.HeaderCell style={{ width: '20%' }}>Default</Table.HeaderCell>
<Table.HeaderCell style={{ width: '14%' }}>
Responsive
</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
{Object.keys(data).map(n => {
const enumValues =
data[n].values === 'icon'
? Object.keys(icons).map(icon => <Chip key={icon}>{icon}</Chip>)
: Array.isArray(data[n].values) &&
data[n].values.map(t => <Chip key={t}>{t}</Chip>);
return (
<Table.Row key={n}>
<Table.Cell style={{ width: '16%' }}>
<Chip head>{n}</Chip>
</Table.Cell>
<Table.Cell style={{ width: '50%' }}>
<div
style={{ display: 'flex', flexWrap: 'wrap', gap: '0.375rem' }}
>
{data[n].type === 'string' && <Chip>string</Chip>}
{data[n].type === 'number' && <Chip>number</Chip>}
{data[n].type === 'boolean' && <Chip>boolean</Chip>}
{data[n].type === 'enum' && enumValues}
{data[n].type === 'spacing' && (
<>
<Chip>0.5, 1, 1.5, 2, 3, ..., 14</Chip>
<Chip>string</Chip>
</>
)}
{data[n].type === 'enum | string' && (
<>
{enumValues}
<Chip>string</Chip>
</>
)}
</div>
</Table.Cell>
<Table.Cell style={{ width: '20%' }}>
<Chip>{data[n].default ? data[n].default : '-'}</Chip>
</Table.Cell>
<Table.Cell style={{ width: '14%' }}>
<Chip>{data[n].responsive ? 'Yes' : 'No'}</Chip>
</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
</Table.Root>
);
};
@@ -0,0 +1 @@
export { PropsTable } from './PropsTable';
@@ -0,0 +1,37 @@
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>
);
};
+1
View File
@@ -0,0 +1 @@
export { Roadmap } from './Roadmap';
+47
View File
@@ -0,0 +1,47 @@
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',
},
];
+100
View File
@@ -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,132 @@
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
display: block;
position: fixed;
top: 16px;
left: 16px;
border-radius: 8px;
width: 300px;
height: calc(100vh - 32px);
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.025);
color: var(--primary);
background-color: var(--panel);
border: 1px solid var(--border);
overflow: hidden;
}
}
.root {
height: 100%;
}
.viewport {
overflow: scroll;
height: 100%;
}
.content {
padding: 0 20px 20px;
}
.logoContainer {
padding-left: 6px;
padding-top: 32px;
}
.logo path {
fill: var(--primary);
}
.menu {
display: flex;
flex-direction: row;
position: relative;
}
.section {
width: 100%;
display: flex;
flex-direction: column;
gap: 2px;
}
.sectionTitle {
font-size: 0.875rem;
font-weight: 600;
padding: 12px 0;
color: var(--primary);
margin-top: 24px;
}
.line {
text-decoration: none;
align-items: center;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 26px;
padding: 0 12px;
border-radius: 4px;
&:hover {
background-color: var(--surface-1);
transition: background-color 0.2s ease-in-out;
}
}
.line.active {
background-color: var(--surface-1);
}
.line.active .lineTitle {
color: var(--primary);
}
.lineTitle {
font-size: 14px;
font-weight: 400;
color: var(--secondary);
}
.lineStatus {
font-size: 14px;
color: var(--secondary);
}
.scrollbar {
display: flex;
justify-content: center;
background-color: rgba(0, 0, 0, 0.1);
width: 0.25rem;
border-radius: 0.375rem;
margin: 0.5rem;
opacity: 0;
transition: opacity 150ms 300ms;
right: -20px;
&[data-hovering],
&[data-scrolling] {
opacity: 1;
transition-duration: 75ms;
transition-delay: 0ms;
}
&::before {
content: '';
position: absolute;
width: 1.25rem;
height: 100%;
}
}
.thumb {
width: 100%;
border-radius: inherit;
background-color: rgba(0, 0, 0, 0.2);
}
+82
View File
@@ -0,0 +1,82 @@
'use client';
import Link from 'next/link';
import { components, overview, layoutComponents, theme } from '@/utils/data';
import { motion } from 'motion/react';
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}>
<div className={styles.sectionTitle}>{section.title}</div>
{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 : ''}`}
>
<div className={styles.lineTitle}>{item.title}</div>
<div className={styles.lineStatus}>
{item.status === 'alpha' && 'Alpha'}
{item.status === 'beta' && 'Beta'}
{item.status === 'inProgress' && 'In Progress'}
{item.status === 'stable' && 'Stable'}
{item.status === 'deprecated' && 'Deprecated'}
</div>
</Link>
);
})}
</Fragment>
);
})}
</motion.div>
);
};
+39
View File
@@ -0,0 +1,39 @@
import styles from './Sidebar.module.css';
import { Docs } from './docs';
import { Playground } from './playground';
import Link from 'next/link';
import { ScrollArea } from '@base-ui-components/react/scroll-area';
export const Sidebar = () => {
return (
<div className={styles.sidebar}>
<ScrollArea.Root className={styles.root}>
<ScrollArea.Viewport className={styles.viewport}>
<div className={styles.content}>
<div className={styles.logoContainer}>
<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>
<div className={styles.menu}>
<Docs />
<Playground />
</div>
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className={styles.scrollbar}>
<ScrollArea.Thumb className={styles.thumb} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</div>
);
};
@@ -0,0 +1,77 @@
'use client';
import { components } from '@/utils/data';
import { Checkbox } from '@backstage/canon';
import { motion } from 'motion/react';
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' }}
>
<div className={styles.sectionTitle}>Components</div>
{components.map(({ slug, title }) => (
<div className={styles.line} key={slug}>
<div className={styles.lineTitle}>{title}</div>
<Checkbox
checked={selectedComponents.includes(slug)}
onChange={() => handleComponentCheckboxChange(slug)}
/>
</div>
))}
<div className={styles.sectionTitle}> Screen sizes</div>
{screenSizes.map(({ slug, title }) => (
<div className={styles.line} key={slug}>
<div className={styles.lineTitle}>{title}</div>
<Checkbox
checked={selectedScreenSizes.includes(slug)}
onChange={() => handleCheckboxChange(slug)}
/>
</div>
))}
</motion.div>
);
};
+51
View File
@@ -0,0 +1,51 @@
'use client';
import { useState } from 'react';
import { Collapsible } from '@base-ui-components/react/collapsible';
import styles from './styles.module.css';
interface SnippetProps {
preview: JSX.Element;
codeContent: JSX.Element;
align?: 'left' | 'center';
px?: number;
py?: number;
open?: boolean;
height?: string | number;
}
export const SnippetClient = ({
preview,
codeContent,
align = 'left',
px = 2,
py = 2,
open = false,
height = 'auto',
}: SnippetProps) => {
const [isOpen, setIsOpen] = useState(open);
return (
<Collapsible.Root
className={styles.container}
defaultOpen={open}
open={isOpen}
onOpenChange={setIsOpen}
>
<div className={styles.preview} style={{ height }}>
<div
className={`${styles.previewContent} ${styles[align]}`}
style={{ padding: `${py}rem ${px}rem` }}
>
{preview}
</div>
<Collapsible.Trigger className={styles.trigger}>
{isOpen ? 'Hide code' : 'View code'}
</Collapsible.Trigger>
</div>
<Collapsible.Panel className={styles.panel}>
{codeContent}
</Collapsible.Panel>
</Collapsible.Root>
);
};
+34
View File
@@ -0,0 +1,34 @@
import { CodeBlock } from '../CodeBlock';
import { SnippetClient } from './client';
interface SnippetProps {
preview: JSX.Element;
code: string;
align?: 'left' | 'center';
px?: number;
py?: number;
open?: boolean;
height?: string | number;
}
export const Snippet = ({
preview,
code = '',
align = 'left',
px = 2,
py = 2,
open = false,
height = 'auto',
}: SnippetProps) => {
return (
<SnippetClient
preview={preview}
codeContent={<CodeBlock code={code} />}
align={align}
px={px}
py={py}
open={open}
height={height}
/>
);
};
@@ -0,0 +1,62 @@
.container {
display: flex;
flex-direction: column;
gap: 8px;
}
.preview {
border-radius: 4px;
box-shadow: inset 0 0 0 1px var(--border);
background-color: var(--bg);
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;
font-size: 14px;
}
[data-theme='dark'] .previewContent {
background-image: radial-gradient(
rgba(255, 255, 255, 0.18) 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;
}
}
+57
View File
@@ -0,0 +1,57 @@
import { CSSProperties, ReactNode } from 'react';
import styles from './styles.module.css';
export const Root = ({ children }: { children: ReactNode }) => {
return (
<div className={styles.wrapper}>
<table className={styles.table}>{children}</table>
</div>
);
};
export const Header = ({ children }: { children: ReactNode }) => {
return <thead>{children}</thead>;
};
export const Body = ({ children }: { children: ReactNode }) => {
return <tbody>{children}</tbody>;
};
export const HeaderRow = ({ children }: { children: ReactNode }) => {
return <tr>{children}</tr>;
};
export const HeaderCell = ({
children,
style,
}: {
children: ReactNode;
style?: CSSProperties;
}) => {
return (
<th
className={`${styles.tableCell} ${styles.tableHeaderCell}`}
style={style}
>
{children}
</th>
);
};
export const Row = ({ children }: { children: ReactNode }) => {
return <tr className={styles.tableRow}>{children}</tr>;
};
export const Cell = ({
children,
style,
}: {
children: ReactNode;
style?: CSSProperties;
}) => {
return (
<td className={styles.tableCell} style={style}>
{children}
</td>
);
};
+1
View File
@@ -0,0 +1 @@
export { Root, Header, Body, Row, Cell, HeaderRow, HeaderCell } from './Table';
@@ -0,0 +1,56 @@
.wrapper {
border: 1px solid var(--border);
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(--panel) !important;
font-size: 16px;
& p {
margin: 0 !important;
}
}
.tableHeaderCell {
border-bottom: 1px solid var(--border) !important;
font-weight: 500;
font-size: 14px;
}
.tableRow {
border: none;
border-bottom: 1px solid var(--border);
&:last-child {
border-bottom: none;
}
}
.tableChip {
display: inline-block;
font-size: 14px !important;
border: 1px solid var(--border);
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';
+42
View File
@@ -0,0 +1,42 @@
'use client';
import { Tabs as TabsPrimitive } from '@base-ui-components/react/tabs';
import styles from './styles.module.css';
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} data-selected={state.selected} {...rest}>
{children}
</button>
);
}}
/>
);
export const Panel = ({
className,
...rest
}: React.ComponentProps<typeof TabsPrimitive.Panel>) => (
<TabsPrimitive.Panel className={`${styles.panel} ${className}`} {...rest} />
);
@@ -0,0 +1,41 @@
.root {
margin-top: 40px;
}
.list {
display: flex;
gap: 24px;
border-bottom: 1px solid var(--border);
position: relative;
margin-bottom: 24px;
}
.tab {
all: unset;
cursor: pointer;
padding-bottom: 12px;
font-size: 14px;
font-weight: 600;
color: var(--primary);
&[data-selected='false'] {
color: var(--secondary);
}
&[data-selected='false']:hover {
color: var(--primary);
transition: color 0.2s ease-in-out;
}
}
.indicator {
position: absolute;
bottom: -1px;
left: var(--active-tab-left);
width: var(--active-tab-width);
height: 1px;
background-color: var(--primary);
transition-property: left, width;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}
+18
View File
@@ -0,0 +1,18 @@
import { ThemeSelector } from './theme';
import { ThemeNameSelector } from './theme-name';
import styles from './styles.module.css';
import { Nav } from './nav';
export const Toolbar = () => {
return (
<div className={styles.toolbar}>
<div>
<Nav />
</div>
<div className={styles.actions}>
<ThemeNameSelector />
<ThemeSelector />
</div>
</div>
);
};
@@ -0,0 +1,78 @@
.tabs {
display: none;
}
@media (min-width: 768px) {
.tabs {
display: block;
height: 60px;
}
}
.tabsTheme {
width: 142px;
border-radius: 0.375rem;
background-color: var(--bg);
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}
.list {
display: flex;
position: relative;
z-index: 0;
gap: 2rem;
}
.tab {
all: unset;
height: 60px;
color: var(--secondary);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
&:hover {
transition: color 0.2s ease-in-out;
color: var(--primary);
}
&[data-selected] {
color: var(--primary);
& p {
color: var(--primary);
}
}
&:focus-visible {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0.25rem 0;
border-radius: 0.25rem;
outline: 2px solid var(--panel);
outline-offset: -1px;
}
}
}
.tab p {
color: var(--secondary) !important;
}
.indicator {
position: absolute;
z-index: -1;
left: 0;
bottom: -1px;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
height: 1px;
border-radius: 0.25rem;
background-color: var(--primary);
transition-property: translate, width;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}
+49
View File
@@ -0,0 +1,49 @@
'use client';
import { Tabs } from '@base-ui-components/react/tabs';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/navigation';
import styles from './nav.module.css';
export const Nav = () => {
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('/');
}}
>
Documentation
</Tabs.Tab>
<Tabs.Tab
className={styles.tab}
value="playground"
onClick={() => {
router.push('/playground');
}}
>
Playground
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
@@ -0,0 +1,22 @@
.toolbar {
position: sticky;
top: 16px;
left: 0;
right: 0;
border-radius: 0.5rem;
z-index: 10;
background-color: var(--panel);
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.5rem;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.025);
border: 1px solid var(--border);
}
.actions {
display: flex;
align-items: center;
gap: 1rem;
}
@@ -0,0 +1,137 @@
.Select {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
height: 2rem;
padding-left: 0.875rem;
padding-right: 0.75rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
font-family: inherit;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
cursor: pointer;
user-select: none;
background-color: var(--surface-1);
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.SelectIcon {
display: flex;
}
.SelectValue {
font-size: 0.875rem;
font-weight: 400;
}
.Positioner {
z-index: 20;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: var(--panel);
color: var(--color-gray-900);
border: 1px solid var(--border);
padding-inline: 0.25rem;
transform-origin: var(--transform-origin);
transition: transform 150ms, opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-side='none'] {
transition: none;
transform: none;
opacity: 1;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow: 0px 10px 15px -3px var(--color-gray-200),
0px 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Item {
box-sizing: border-box;
outline: 0;
font-size: 0.875rem;
line-height: 1rem;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 1rem;
font-size: 0.875rem;
font-weight: 600;
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
cursor: default;
user-select: none;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
[data-side='none'] & {
font-size: 1rem;
padding-right: 3rem;
min-width: calc(var(--anchor-width) + 1rem);
}
&[data-highlighted] {
z-index: 0;
position: relative;
color: var(--color-gray-50);
background-color: var(--bg);
}
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
}
}
.ItemIndicator {
grid-column-start: 1;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
}
.ItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.ItemText {
grid-column-start: 2;
font-size: 14px;
}
@@ -0,0 +1,49 @@
'use client';
import { Select } from '@base-ui-components/react/select';
import styles from './theme-name.module.css';
import { Icon } from '@backstage/canon';
import { usePlayground } from '@/utils/playground-context';
const themes = [
{ name: 'Backstage', value: 'default' },
{ name: 'Spotify', value: 'spotify' },
{ name: 'Custom theme', value: 'custom' },
];
export const ThemeNameSelector = () => {
const { selectedThemeName, setSelectedThemeName } = usePlayground();
return (
<Select.Root
value={selectedThemeName || 'default'}
onValueChange={setSelectedThemeName}
>
<Select.Trigger className={styles.Select}>
<Select.Value
className={styles.SelectValue}
placeholder="Select a theme"
/>
<Select.Icon className={styles.SelectIcon}>
<Icon name="chevron-down" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className={styles.Positioner} sideOffset={8}>
<Select.Popup className={styles.Popup}>
{themes.map(({ name, value }) => (
<Select.Item className={styles.Item} value={value} key={value}>
<Select.ItemIndicator className={styles.ItemIndicator}>
<Icon name="check" />
</Select.ItemIndicator>
<Select.ItemText className={styles.ItemText}>
{name}
</Select.ItemText>
</Select.Item>
))}
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
);
};
@@ -0,0 +1,81 @@
.tabs {
border-radius: 0.375rem;
width: 100%;
background-color: var(--surface-1);
}
.tabsTheme {
width: 100px;
border-radius: 0.375rem;
background-color: var(--surface-1);
}
.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(--secondary);
user-select: none;
height: 2rem;
flex: 1;
cursor: pointer;
&[data-selected] {
color: var(--primary);
& p {
color: var(--primary);
}
}
@media (hover: hover) {
&:hover {
color: var(--primary);
}
}
&:focus-visible {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0.25rem 0;
border-radius: 0.25rem;
outline: 2px solid var(--panel);
outline-offset: -1px;
}
}
}
.tab p {
color: var(--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.5rem;
border-radius: 0.25rem;
background-color: var(--panel);
transition-property: translate, width;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}
+28
View File
@@ -0,0 +1,28 @@
'use client';
import { Tabs } from '@base-ui-components/react/tabs';
import { Icon } from '@backstage/canon';
import { usePlayground } from '@/utils/playground-context';
import styles from './theme.module.css';
export const ThemeSelector = () => {
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>
);
};
+65
View File
@@ -0,0 +1,65 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { AvatarSnippet } from '@/snippets/stories-snippets';
import {
avatarPropDefs,
snippetUsage,
snippetSizes,
snippetFallback,
} from './avatar.props';
import { ComponentInfos } from '@/components/ComponentInfos';
# Avatar
An avatar component with a fallback for initials.
<Snippet
align="center"
py={4}
preview={<AvatarSnippet story="Default" />}
code={`<Avatar src="https://avatars.githubusercontent.com/u/1540635?v=4" name="Charles de Dreuille" />`}
/>
<ComponentInfos
component="avatar"
usageCode={snippetUsage}
classNames={[
'canon-AvatarRoot',
'canon-AvatarRoot[data-size="small"]',
'canon-AvatarRoot[data-size="medium"]',
'canon-AvatarRoot[data-size="large"]',
'canon-AvatarImage',
'canon-AvatarFallback',
]}
/>
## API reference
<PropsTable data={avatarPropDefs} />
## Examples
### Sizes
Avatar sizes can be set using the `size` prop.
<Snippet
align="center"
py={4}
open
preview={<AvatarSnippet story="Sizes" />}
code={snippetSizes}
/>
### Fallback
If the image is not available, the avatar will show the initials of the name.
<Snippet
align="center"
py={4}
open
preview={<AvatarSnippet story="Fallback" />}
code={snippetFallback}
/>
@@ -0,0 +1,46 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const avatarPropDefs: Record<string, PropDef> = {
src: {
type: 'string',
},
name: {
type: 'string',
},
size: {
type: 'enum',
values: ['small', 'medium', 'large'],
default: 'medium',
responsive: true,
},
...classNamePropDefs,
...stylePropDefs,
};
export const snippetUsage = `import { Avatar } from '@backstage/canon';
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille"
/>`;
export const snippetSizes = `<Flex gap="4" direction="column">
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille" size="small"
/>
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille" size="medium"
/>
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille" size="large"
/>
</Flex>`;
export const snippetFallback = `<Avatar
src="https://avatars.githubusercontent.com/u/15406AAAAAAAAA"
name="Charles de Dreuille"
/>`;
+55
View File
@@ -0,0 +1,55 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { BoxSnippet } from '@/snippets/stories-snippets';
import {
boxPropDefs,
snippetUsage,
boxPreviewSnippet,
boxSimpleSnippet,
boxResponsiveSnippet,
} from './box.props';
import { spacingPropDefs } from '@/utils/propDefs';
import { ComponentInfos } from '@/components/ComponentInfos';
# Box
Box is the lowest-level component in Canon. It provides a consistent API for styling and layout.
<Snippet
py={4}
preview={<BoxSnippet story="Preview" />}
code={boxPreviewSnippet}
align="center"
/>
<ComponentInfos component="box" usageCode={snippetUsage} />
## API reference
### Box
This is the Box component, our lowest-level component. Here are all the
available properties.
<PropsTable data={boxPropDefs} />
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={spacingPropDefs} />
## Examples
### Simple example
A simple example of how to use the Box component.
<CodeBlock code={boxSimpleSnippet} />
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={boxResponsiveSnippet} />
@@ -0,0 +1,40 @@
import {
classNamePropDefs,
displayPropDefs,
heightPropDefs,
positionPropDefs,
stylePropDefs,
widthPropDefs,
type PropDef,
} from '@/utils/propDefs';
export const boxPropDefs: Record<string, PropDef> = {
as: {
type: 'enum',
values: ['div', 'span'],
default: 'div',
responsive: true,
},
...widthPropDefs,
...heightPropDefs,
...positionPropDefs,
...displayPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const snippetUsage = `import { Box } from '@backstage/canon';
<Box />`;
export const boxPreviewSnippet = `<Box>
<DecorativeBox />
</Box>`;
export const boxSimpleSnippet = `<Box padding="md" borderRadius="md">Hello World</Box>`;
export const boxResponsiveSnippet = `<Box
padding={{ xs: 'sm', md: 'md' }}
borderRadius={{ xs: 'sm', md: 'md' }}>
Hello World
</Box>`;
@@ -0,0 +1,80 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ButtonIconSnippet } from '@/snippets/stories-snippets';
import {
buttonIconPropDefs,
buttonIconUsageSnippet,
buttonIconDefaultSnippet,
buttonIconVariantsSnippet,
buttonIconSizesSnippet,
buttonIconDisabledSnippet,
buttonIconResponsiveSnippet,
buttonIconAsLinkSnippet,
} from './button-icon.props';
import { ComponentInfos } from '@/components/ComponentInfos';
# ButtonIcon
A button component with a single icon that can be used to trigger actions.
<Snippet
align="center"
py={4}
preview={<ButtonIconSnippet story="Variants" />}
code={buttonIconDefaultSnippet}
/>
<ComponentInfos
component="button-icon"
classNames={['canon-Button', 'canon-ButtonIcon']}
usageCode={buttonIconUsageSnippet}
/>
## API reference
<PropsTable data={buttonIconPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Variants" />}
code={buttonIconVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Sizes" />}
code={buttonIconSizesSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Disabled" />}
code={buttonIconDisabledSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonIconResponsiveSnippet} />
@@ -0,0 +1,59 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const buttonIconPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
icon: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
default: 'button',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/canon';
<ButtonIcon />`;
export const buttonIconDefaultSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="secondary" />
</Flex>`;
export const buttonIconVariantsSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="secondary" />
</Flex>`;
export const buttonIconSizesSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} size="small" />
<ButtonIcon icon={<Icon name="cloud" />} size="medium" />
</Flex>`;
export const buttonIconDisabledSnippet = `<ButtonIcon icon={<Icon name="cloud" />} isDisabled />`;
export const buttonIconResponsiveSnippet = `<ButtonIcon icon={<Icon name="cloud" />} variant={{ initial: 'primary', lg: 'secondary' }} />`;
export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/canon';
<ButtonLink href="https://canon.backstage.io" target="_blank">
Button
</ButtonLink>`;
@@ -0,0 +1,91 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ButtonLinkSnippet } from '@/snippets/stories-snippets';
import {
buttonLinkPropDefs,
buttonLinkSnippetUsage,
buttonLinkVariantsSnippet,
buttonLinkSizesSnippet,
buttonLinkIconsSnippet,
buttonLinkDisabledSnippet,
buttonLinkResponsiveSnippet,
} from './button-link.props';
import { ComponentInfos } from '@/components/ComponentInfos';
# ButtonLink
A button component that can be used as a link.
<Snippet
align="center"
py={4}
preview={<ButtonLinkSnippet story="Variants" />}
code={buttonLinkVariantsSnippet}
/>
<ComponentInfos
component="button-link"
classNames={['canon-Button', 'canon-ButtonLink']}
usageCode={buttonLinkSnippetUsage}
/>
## API reference
<PropsTable data={buttonLinkPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Variants" />}
code={buttonLinkVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Sizes" />}
code={buttonLinkSizesSnippet}
/>
### With Icons
Here's a view when buttons have icons.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="WithIcons" />}
code={buttonLinkIconsSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Disabled" />}
code={buttonLinkDisabledSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonLinkResponsiveSnippet} />
@@ -0,0 +1,73 @@
import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs';
import type { PropDef } from '../../utils/propDefs';
export const buttonLinkPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
href: { type: 'string', responsive: false },
hrefLang: { type: 'string', responsive: false },
target: {
type: 'enum',
values: ['HTMLAttributeAnchorTarget'],
default: '_self',
responsive: false,
},
rel: { type: 'string', responsive: false },
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/canon';
<ButtonLink />`;
export const buttonLinkVariantsSnippet = `<Flex align="center">
<ButtonLink iconStart={<Icon name="cloud" />} variant="primary">
Button
</ButtonLink>
<ButtonLink iconStart={<Icon name="cloud" />} variant="secondary">
Button
</ButtonLink>
</Flex>`;
export const buttonLinkSizesSnippet = `<Flex align="center">
<ButtonLink size="small">Small</ButtonLink>
<ButtonLink size="medium">Medium</ButtonLink>
</Flex>`;
export const buttonLinkIconsSnippet = `<Flex align="center">
<ButtonLink iconStart={<Icon name="cloud" />}>Button</ButtonLink>
<ButtonLink iconEnd={<Icon name="chevronRight" />}>Button</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevronRight" />}>
Button
</ButtonLink>
</Flex>`;
export const buttonLinkDisabledSnippet = `<Flex gap="4">
<ButtonLink variant="primary" isDisabled>
Primary
</ButtonLink>
<ButtonLink variant="secondary" isDisabled>
Secondary
</ButtonLink>
</Flex>`;
export const buttonLinkResponsiveSnippet = `<ButtonLink variant={{ initial: 'primary', lg: 'secondary' }}>
Responsive Button
</ButtonLink>`;
+104
View File
@@ -0,0 +1,104 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ButtonSnippet, ButtonLinkSnippet } from '@/snippets/stories-snippets';
import {
buttonPropDefs,
buttonSnippetUsage,
buttonVariantsSnippet,
buttonSizesSnippet,
buttonIconsSnippet,
buttonDisabledSnippet,
buttonResponsiveSnippet,
buttonAsLinkSnippet,
} from './button.props';
import { ComponentInfos } from '@/components/ComponentInfos';
# Button
A button component that can be used to trigger actions.
<Snippet
align="center"
py={4}
preview={<ButtonSnippet story="Variants" />}
code={buttonVariantsSnippet}
/>
<ComponentInfos
component="button"
classNames={['canon-Button']}
usageCode={buttonSnippetUsage}
/>
## API reference
<PropsTable data={buttonPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<ButtonSnippet story="Variants" />}
code={buttonVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<ButtonSnippet story="Sizes" />}
code={buttonSizesSnippet}
/>
### With Icons
Here's a view when buttons have icons.
<Snippet
align="center"
py={4}
open
preview={<ButtonSnippet story="WithIcons" />}
code={buttonIconsSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<ButtonSnippet story="Disabled" />}
code={buttonDisabledSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonResponsiveSnippet} />
### As Link
If you want to use a button as a link, please use the `ButtonLink` component.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Variants" />}
code={buttonAsLinkSnippet}
/>
@@ -0,0 +1,72 @@
import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs';
import type { PropDef } from '../../utils/propDefs';
export const buttonPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
children: { type: 'enum', values: ['ReactNode'], responsive: false },
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
default: 'button',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const buttonSnippetUsage = `import { Button } from '@backstage/canon';
<Button />`;
export const buttonVariantsSnippet = `<Flex align="center">
<Button iconStart="cloud" variant="primary">
Button
</Button>
<Button iconStart="cloud" variant="secondary">
Button
</Button>
</Flex>`;
export const buttonSizesSnippet = `<Flex align="center">
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
</Flex>`;
export const buttonIconsSnippet = `<Flex align="center">
<Button iconStart={<Icon name="cloud" />}>Button</Button>
<Button iconEnd={<Icon name="chevronRight" />}>Button</Button>
<Button iconStart={<Icon name="cloud" />} iconEnd={<Icon name="chevronRight" />}>Button</Button>
</Flex>`;
export const buttonDisabledSnippet = `<Flex gap="4">
<Button variant="primary" isDisabled>
Primary
</Button>
<Button variant="secondary" isDisabled>
Secondary
</Button>
</Flex>`;
export const buttonResponsiveSnippet = `<Button variant={{ initial: 'primary', lg: 'secondary' }}>
Responsive Button
</Button>`;
export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/canon';
<ButtonLink href="https://canon.backstage.io" target="_blank">
Button
</ButtonLink>`;

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