diff --git a/packages/canon/docs/Home.mdx b/packages/canon/docs/Home.mdx
index 479834303a..3a00de5d07 100644
--- a/packages/canon/docs/Home.mdx
+++ b/packages/canon/docs/Home.mdx
@@ -1,5 +1,13 @@
import { Unstyled } from '@storybook/blocks';
-import { Columns, Text, ComponentStatus, Banner, Title } from './components';
+import {
+ Columns,
+ Text,
+ ComponentStatus,
+ Banner,
+ Title,
+ Roadmap,
+} from './components';
+import { list } from './components/Roadmap/list';
@@ -19,7 +27,7 @@ import { Columns, Text, ComponentStatus, Banner, Title } from './components';
- Project Status
+ Component Status
We are still in the process of documenting the API and building the
@@ -29,51 +37,45 @@ import { Columns, Text, ComponentStatus, Banner, Title } from './components';
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Roadmap
+
+
+
+
diff --git a/packages/canon/docs/components/Columns/columns.css.ts b/packages/canon/docs/components/Columns/columns.css.ts
index 6efe97c349..0b5ec18aef 100644
--- a/packages/canon/docs/components/Columns/columns.css.ts
+++ b/packages/canon/docs/components/Columns/columns.css.ts
@@ -18,7 +18,7 @@ import { style } from '@vanilla-extract/css';
export const styles = style({
display: 'grid',
- gridTemplateColumns: 'repeat(2, 1fr)',
+ gridTemplateColumns: 'repeat(3, 1fr)',
rowGap: '20px',
columnGap: '80px',
});
diff --git a/packages/canon/docs/components/ComponentStatus/component-status.css.ts b/packages/canon/docs/components/ComponentStatus/component-status.css.ts
index eeb58865ff..5cce5adb5d 100644
--- a/packages/canon/docs/components/ComponentStatus/component-status.css.ts
+++ b/packages/canon/docs/components/ComponentStatus/component-status.css.ts
@@ -57,7 +57,17 @@ export const pill = recipe({
borderColor: '#FFD000',
color: '#D79927',
},
- done: {
+ alpha: {
+ backgroundColor: '#D7F9D7',
+ borderColor: '#4ED14A',
+ color: '#3A9837',
+ },
+ beta: {
+ backgroundColor: '#D7F9D7',
+ borderColor: '#4ED14A',
+ color: '#3A9837',
+ },
+ stable: {
backgroundColor: '#D7F9D7',
borderColor: '#4ED14A',
color: '#3A9837',
diff --git a/packages/canon/docs/components/ComponentStatus/index.tsx b/packages/canon/docs/components/ComponentStatus/index.tsx
index 2444fcdc16..64d552ea97 100644
--- a/packages/canon/docs/components/ComponentStatus/index.tsx
+++ b/packages/canon/docs/components/ComponentStatus/index.tsx
@@ -24,7 +24,7 @@ export const ComponentStatus = ({
link,
}: {
name: string;
- status: 'notStarted' | 'inProgress' | 'done';
+ status: 'notStarted' | 'inProgress' | 'alpha' | 'beta' | 'stable';
style?: React.CSSProperties;
link?: string;
}) => {
@@ -40,7 +40,9 @@ export const ComponentStatus = ({
{status === 'notStarted' && 'Not Started'}
{status === 'inProgress' && 'In Progress'}
- {status === 'done' && 'Done'}
+ {status === 'alpha' && 'Alpha'}
+ {status === 'beta' && 'Beta'}
+ {status === 'stable' && 'Stable'}
);
diff --git a/packages/canon/docs/components/Roadmap/index.tsx b/packages/canon/docs/components/Roadmap/index.tsx
new file mode 100644
index 0000000000..2177ea558f
--- /dev/null
+++ b/packages/canon/docs/components/Roadmap/index.tsx
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { RoadmapItem } from './list';
+
+export const Roadmap = ({ list }: { list: RoadmapItem[] }) => {
+ const orderList = ['inProgress', 'notStarted', 'completed'];
+ return (
+
+ {list
+ .sort(
+ (a, b) => orderList.indexOf(a.status) - orderList.indexOf(b.status),
+ )
+ .map(Item)}
+
+ );
+};
+
+const Item = ({
+ title,
+ status = 'notStarted',
+}: {
+ title: string;
+ status: 'notStarted' | 'inProgress' | 'inReview' | 'completed';
+}) => {
+ return (
+
+
+
+ {status === 'notStarted' && 'Not Started'}
+ {status === 'inProgress' && 'In Progress'}
+ {status === 'inReview' && 'Ready for Review'}
+ {status === 'completed' && 'Completed'}
+
+
+ );
+};
diff --git a/packages/canon/docs/components/Roadmap/list.ts b/packages/canon/docs/components/Roadmap/list.ts
new file mode 100644
index 0000000000..d5f436281a
--- /dev/null
+++ b/packages/canon/docs/components/Roadmap/list.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export type RoadmapItem = {
+ title: string;
+ status: 'notStarted' | 'inProgress' | 'inReview' | 'completed';
+};
+
+export const list: RoadmapItem[] = [
+ {
+ title: 'Remove Vanilla Extract and use pure CSS instead',
+ status: 'inProgress',
+ },
+ {
+ title: 'Add collapsing across breakpoints for the Inline component',
+ status: 'notStarted',
+ },
+ {
+ title: 'Add reversing the order for the Inline component',
+ status: 'notStarted',
+ },
+ {
+ title: 'Set up Storybook',
+ status: 'completed',
+ },
+ {
+ title: 'Set up iconography',
+ status: 'completed',
+ },
+ {
+ title: 'Set up global tokens',
+ status: 'inProgress',
+ },
+ {
+ title: 'Set up theming system',
+ status: 'inProgress',
+ },
+ {
+ title: 'Create first pass at box component',
+ status: 'completed',
+ },
+ {
+ title: 'Create first pass at stack component',
+ status: 'completed',
+ },
+ {
+ title: 'Create first pass at inline component',
+ status: 'completed',
+ },
+];
diff --git a/packages/canon/docs/components/Roadmap/styles.css b/packages/canon/docs/components/Roadmap/styles.css
new file mode 100644
index 0000000000..e47aecf7fa
--- /dev/null
+++ b/packages/canon/docs/components/Roadmap/styles.css
@@ -0,0 +1,102 @@
+.roadmap {
+ display: flex;
+ flex-direction: column;
+}
+
+.roadmap .roadmap-item {
+ font-family: 'Geist', sans-serif;
+ 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-weight: 300;
+ 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;
+}
diff --git a/packages/canon/docs/components/index.ts b/packages/canon/docs/components/index.ts
index 88a8cb0391..7ad07bec6a 100644
--- a/packages/canon/docs/components/index.ts
+++ b/packages/canon/docs/components/index.ts
@@ -23,3 +23,4 @@ export * from './ComponentStatus';
export * from './LayoutComponents';
export * from './Banner';
export * from './IconLibrary';
+export * from './Roadmap';
diff --git a/packages/canon/docs/components/styles.css b/packages/canon/docs/components/styles.css
index 63df601609..698085ad19 100644
--- a/packages/canon/docs/components/styles.css
+++ b/packages/canon/docs/components/styles.css
@@ -1 +1,2 @@
@import './IconLibrary/styles.css';
+@import './Roadmap/styles.css';
diff --git a/packages/canon/src/utils/list-values.ts b/packages/canon/docs/utils/argTypes.ts
similarity index 55%
rename from packages/canon/src/utils/list-values.ts
rename to packages/canon/docs/utils/argTypes.ts
index 9e3464a4f5..ff5a159677 100644
--- a/packages/canon/src/utils/list-values.ts
+++ b/packages/canon/docs/utils/argTypes.ts
@@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import { boxProperties } from '../../src/components/Box/sprinkles.css';
import {
- responsiveProperties,
colorProperties,
-} from '../components/Box/sprinkles.css';
+ spacingProperties,
+} from '../../src/layout/sprinkles.css';
export const listResponsiveValues = (
- value: keyof typeof responsiveProperties.styles,
+ value: keyof typeof boxProperties.styles,
) => {
- const values = responsiveProperties.styles[value];
+ const values = boxProperties.styles[value];
if ('values' in values) {
return Object.keys(values.values);
@@ -40,3 +40,25 @@ export const listColorValues = (value: keyof typeof colorProperties.styles) => {
return [];
};
+
+export const argTypesSpacing = Object.keys(spacingProperties.styles).reduce<
+ Record
+>((acc, n) => {
+ acc[n] = {
+ control: 'inline-radio',
+ options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'],
+ };
+ return acc;
+}, {});
+
+export const argTypesColor = Object.keys(colorProperties.styles).reduce<
+ Record
+>((acc, n) => {
+ acc[n as keyof typeof colorProperties.styles] = {
+ control: 'select',
+ options: Object.keys(
+ colorProperties.styles[n as keyof typeof colorProperties.styles].values,
+ ),
+ };
+ return acc;
+}, {});
diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md
index 723e08859b..e98d57a13d 100644
--- a/packages/canon/report.api.md
+++ b/packages/canon/report.api.md
@@ -6,12 +6,12 @@
///
import { CSSProperties } from 'react';
-import { JSXElementConstructor } from 'react';
+import { DOMElement } from 'react';
import { default as React_2 } from 'react';
-import { ReactElement } from 'react';
+import { ReactNode } from 'react';
// @public (undocumented)
-export type AlignItems =
+export type AlignItemsProps =
| 'stretch'
| 'flex-start'
| 'center'
@@ -20,6 +20,25 @@ export type AlignItems =
Record
>;
+// @public (undocumented)
+export type AsProps =
+ | 'div'
+ | 'span'
+ | 'p'
+ | 'article'
+ | 'section'
+ | 'main'
+ | 'nav'
+ | 'aside'
+ | 'ul'
+ | 'ol'
+ | 'li'
+ | 'details'
+ | 'summary'
+ | 'dd'
+ | 'dl'
+ | 'dt';
+
// @public (undocumented)
export type Background =
| 'background'
@@ -31,7 +50,7 @@ export type Background =
>;
// @public (undocumented)
-export type BorderRadius =
+export type BorderRadiusProps =
| 'none'
| 'small'
| 'medium'
@@ -39,68 +58,35 @@ export type BorderRadius =
| Partial>;
// @public (undocumented)
-export const Box: (props: BoxProps) => ReactElement<
+export const Box: (props: BoxProps) => DOMElement<
{
className: string;
style: CSSProperties | undefined;
+ children: ReactNode;
},
- string | JSXElementConstructor
+ Element
>;
// @public (undocumented)
-export interface BoxProps {
+export interface BoxProps extends SpaceProps, ColorProps {
// (undocumented)
- alignItems?: AlignItems;
+ alignItems?: AlignItemsProps;
// (undocumented)
as?: keyof JSX.IntrinsicElements;
// (undocumented)
- background?: Background;
- // (undocumented)
- borderRadius?: BorderRadius;
+ borderRadius?: BorderRadiusProps;
// (undocumented)
children?: React.ReactNode;
// (undocumented)
className?: string;
// (undocumented)
- color?: Color;
+ display?: DisplayProps;
// (undocumented)
- display?: Display;
+ flexDirection?: FlexDirectionProps;
// (undocumented)
- flexDirection?: FlexDirection;
+ flexWrap?: FlexWrapProps;
// (undocumented)
- flexWrap?: FlexWrap;
- // (undocumented)
- gap?: Gap;
- // (undocumented)
- justifyContent?: JustifyContent;
- // (undocumented)
- margin?: Margin;
- // (undocumented)
- marginBottom?: MarginBottom;
- // (undocumented)
- marginLeft?: MarginLeft;
- // (undocumented)
- marginRight?: MarginRight;
- // (undocumented)
- marginTop?: MarginTop;
- // (undocumented)
- marginX?: MarginX;
- // (undocumented)
- marginY?: MarginY;
- // (undocumented)
- padding?: Padding;
- // (undocumented)
- paddingBottom?: PaddingBottom;
- // (undocumented)
- paddingLeft?: PaddingLeft;
- // (undocumented)
- paddingRight?: PaddingRight;
- // (undocumented)
- paddingTop?: PaddingTop;
- // (undocumented)
- paddingX?: PaddingX;
- // (undocumented)
- paddingY?: PaddingY;
+ justifyContent?: JustifyContentProps;
// (undocumented)
style?: React.CSSProperties;
}
@@ -163,7 +149,15 @@ export type Color =
| Partial>;
// @public (undocumented)
-export type Display =
+export interface ColorProps {
+ // (undocumented)
+ background?: Background;
+ // (undocumented)
+ color?: Color;
+}
+
+// @public (undocumented)
+export type DisplayProps =
| 'flex'
| 'none'
| 'inline'
@@ -171,13 +165,13 @@ export type Display =
| Partial>;
// @public (undocumented)
-export type FlexDirection =
+export type FlexDirectionProps =
| 'row'
| 'column'
| Partial>;
// @public (undocumented)
-export type FlexWrap =
+export type FlexWrapProps =
| 'wrap'
| 'nowrap'
| Partial>;
@@ -203,7 +197,7 @@ export type IconNames =
| 'trash';
// @public (undocumented)
-export type JustifyContent =
+export type JustifyContentProps =
| 'stretch'
| 'flex-start'
| 'center'
@@ -279,6 +273,40 @@ export const space: {
xxl: string;
};
+// @public (undocumented)
+export interface SpaceProps {
+ // (undocumented)
+ gap?: Gap;
+ // (undocumented)
+ margin?: Margin;
+ // (undocumented)
+ marginBottom?: MarginBottom;
+ // (undocumented)
+ marginLeft?: MarginLeft;
+ // (undocumented)
+ marginRight?: MarginRight;
+ // (undocumented)
+ marginTop?: MarginTop;
+ // (undocumented)
+ marginX?: MarginX;
+ // (undocumented)
+ marginY?: MarginY;
+ // (undocumented)
+ padding?: Padding;
+ // (undocumented)
+ paddingBottom?: PaddingBottom;
+ // (undocumented)
+ paddingLeft?: PaddingLeft;
+ // (undocumented)
+ paddingRight?: PaddingRight;
+ // (undocumented)
+ paddingTop?: PaddingTop;
+ // (undocumented)
+ paddingX?: PaddingX;
+ // (undocumented)
+ paddingY?: PaddingY;
+}
+
// @public (undocumented)
export type Theme = keyof typeof themes;
diff --git a/packages/canon/src/components/Box/Box.stories.tsx b/packages/canon/src/components/Box/Box.stories.tsx
index 8b8a2d7c56..6f15d984e1 100644
--- a/packages/canon/src/components/Box/Box.stories.tsx
+++ b/packages/canon/src/components/Box/Box.stories.tsx
@@ -17,87 +17,41 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Box } from './Box';
-import { listResponsiveValues } from '../../utils/list-values';
-import { responsiveProperties, colorProperties } from './sprinkles.css';
+import {
+ listResponsiveValues,
+ argTypesSpacing,
+ argTypesColor,
+} from '../../../docs/utils/argTypes';
+import { boxProperties } from './sprinkles.css';
-const argTypesResponsive = Object.keys(responsiveProperties.styles).reduce<
+const argTypesBox = Object.keys(boxProperties.styles).reduce<
Record
>((acc, n) => {
- if (
- [
- 'margin',
- 'marginBottom',
- 'marginLeft',
- 'marginRight',
- 'marginTop',
- 'marginX',
- 'marginY',
- 'padding',
- 'paddingBottom',
- 'paddingLeft',
- 'paddingRight',
- 'paddingTop',
- 'paddingX',
- 'paddingY',
- ].includes(n)
- ) {
- acc[n] = {
- control: 'select',
- options: ['xs', 'sm', 'md', 'lg', 'xl', '2xl'],
- };
- } else {
- acc[n] = {
- control: 'select',
- options: listResponsiveValues(
- n as keyof typeof responsiveProperties.styles,
- ),
- };
- }
- return acc;
-}, {});
-
-const argTypesColor = Object.keys(colorProperties.styles).reduce<
- Record
->((acc, n) => {
- acc[n as keyof typeof colorProperties.styles] = {
+ acc[n] = {
control: 'select',
- options: Object.keys(
- colorProperties.styles[n as keyof typeof colorProperties.styles].values,
- ),
+ options: listResponsiveValues(n as keyof typeof boxProperties.styles),
};
return acc;
}, {});
-const argTypes = {
- ...argTypesResponsive,
- ...argTypesColor,
-};
-
-// Sort the resulting object keys alphabetically
-const sortedArgTypes = Object.keys(argTypes)
- .sort()
- .reduce((acc, key) => {
- acc[key] = argTypes[key];
- return acc;
- }, {} as Record);
-
-// Add 'as' and 'children' to the sortedArgTypes
-sortedArgTypes['as'] = {
- control: 'select',
- options: ['div', 'span', 'article', 'section'],
-};
-
-sortedArgTypes['children'] = {
- control: 'text',
-};
-
const meta = {
title: 'Components/Box',
component: Box,
parameters: {
layout: 'centered',
},
- argTypes: sortedArgTypes,
+ argTypes: {
+ ...argTypesSpacing,
+ ...argTypesColor,
+ ...argTypesBox,
+ as: {
+ control: { type: 'select' },
+ options: ['div', 'span', 'article', 'section'],
+ },
+ children: {
+ control: false,
+ },
+ },
args: {
as: 'div',
background: 'elevation1',
diff --git a/packages/canon/src/components/Box/Box.tsx b/packages/canon/src/components/Box/Box.tsx
index 960220a949..d134abff35 100644
--- a/packages/canon/src/components/Box/Box.tsx
+++ b/packages/canon/src/components/Box/Box.tsx
@@ -21,30 +21,19 @@ import { BoxProps } from './types';
/** @public */
export const Box = (props: BoxProps) => {
- const { as = 'div', className, style, ...rest } = props;
- const boxSprinklesProps: Record = {};
- const nativeProps: Record = {};
+ const { as = 'div', className, style, children, ...restProps } = props;
- // Split props between sprinkles and native HTML props
- Object.entries(rest).forEach(([key, value]) => {
- if (value === undefined) return;
+ // Generate the list of class names
+ const sprinklesClassName = boxSprinkles(restProps);
- if (
- boxSprinkles.properties.has(
- key as keyof Parameters[0],
- )
- ) {
- boxSprinklesProps[key] = value;
- } else {
- nativeProps[key] = value;
- }
- });
-
- const sprinklesClassName = boxSprinkles(boxSprinklesProps);
+ // Combine the base class name, the sprinkles class name, and any additional class names
+ const classNames = [base, sprinklesClassName, className]
+ .filter(Boolean)
+ .join(' ');
return createElement(as, {
- className: [base, sprinklesClassName, className].filter(Boolean).join(' '),
+ className: classNames,
style,
- ...nativeProps,
+ children,
});
};
diff --git a/packages/canon/src/components/Box/Docs.mdx b/packages/canon/src/components/Box/Docs.mdx
index a3ce48b06d..dac10952cf 100644
--- a/packages/canon/src/components/Box/Docs.mdx
+++ b/packages/canon/src/components/Box/Docs.mdx
@@ -1,13 +1,7 @@
import { Canvas, Meta, Unstyled } from '@storybook/blocks';
import * as BoxStories from './Box.stories';
-import { Chip } from '../../../docs/components/Chip';
-import * as Table from '../../../docs/components/Table';
-import { Box } from './Box';
-import { Stack } from '../Stack';
import { Title, Text } from '../../../docs/components';
import { Padding } from './docs/padding';
-import { listResponsiveValues, listColorValues } from '../../utils/list-values';
-import { responsiveProperties } from './sprinkles.css';
import { PropsTable } from './docs/props-table';
import { SpacingTable } from './docs/spacing-table';
diff --git a/packages/canon/src/components/Box/docs/padding.tsx b/packages/canon/src/components/Box/docs/padding.tsx
index 3f78f56692..86a7c801a2 100644
--- a/packages/canon/src/components/Box/docs/padding.tsx
+++ b/packages/canon/src/components/Box/docs/padding.tsx
@@ -15,7 +15,6 @@
*/
import React from 'react';
-import { Stack } from '../../Stack';
import { Inline } from '../../Inline';
import { Box } from '../Box';
@@ -32,74 +31,77 @@ const FakeBox = ({ children }: { children: string }) => (
export const Padding = () => {
return (
-
-
-
+
padding
-
-
+
paddingX
-
-
+
paddingY
-
+
-
-
+
paddingTop
-
-
+
paddingBottom
-
-
+
paddingLeft
-
-
+
paddingRight
-
+
-
+
);
};
diff --git a/packages/canon/src/components/Box/docs/props-table.tsx b/packages/canon/src/components/Box/docs/props-table.tsx
index bcdda93f2a..9979701f2e 100644
--- a/packages/canon/src/components/Box/docs/props-table.tsx
+++ b/packages/canon/src/components/Box/docs/props-table.tsx
@@ -17,8 +17,8 @@
import React from 'react';
import * as Table from '../../../../docs/components';
import { Chip } from '../../../../docs/components';
-import { listResponsiveValues } from '../../../utils/list-values';
-import { responsiveProperties } from '../sprinkles.css';
+import { listResponsiveValues } from '../../../../docs/utils/argTypes';
+import { boxProperties } from '../sprinkles.css';
export const PropsTable = () => {
return (
@@ -30,7 +30,7 @@ export const PropsTable = () => {
- {Object.keys(responsiveProperties.styles)
+ {Object.keys(boxProperties.styles)
.filter(
n =>
![
@@ -57,7 +57,7 @@ export const PropsTable = () => {
{listResponsiveValues(
- n as keyof typeof responsiveProperties.styles,
+ n as keyof typeof boxProperties.styles,
).map(value => (
{value}
))}
diff --git a/packages/canon/src/components/Box/docs/spacing-table.tsx b/packages/canon/src/components/Box/docs/spacing-table.tsx
index 32b1eac1b9..93c38457d4 100644
--- a/packages/canon/src/components/Box/docs/spacing-table.tsx
+++ b/packages/canon/src/components/Box/docs/spacing-table.tsx
@@ -17,8 +17,8 @@
import React from 'react';
import * as Table from '../../../../docs/components';
import { Chip } from '../../../../docs/components';
-import { listResponsiveValues } from '../../../utils/list-values';
-import { responsiveProperties } from '../sprinkles.css';
+import { spacingProperties } from '../../../layout/sprinkles.css';
+import { space } from '../../../layout/properties';
export const SpacingTable = () => {
return (
@@ -30,7 +30,7 @@ export const SpacingTable = () => {
- {Object.keys(responsiveProperties.styles)
+ {Object.keys(spacingProperties.styles)
.filter(n =>
[
'padding',
@@ -55,9 +55,7 @@ export const SpacingTable = () => {
{n}
- {listResponsiveValues(
- 'paddingTop' as keyof typeof responsiveProperties.styles,
- ).map(value => (
+ {Object.keys(space).map(value => (
{value}
))}
diff --git a/packages/canon/src/components/Box/index.tsx b/packages/canon/src/components/Box/index.tsx
index f275aa352c..e4f1867edc 100644
--- a/packages/canon/src/components/Box/index.tsx
+++ b/packages/canon/src/components/Box/index.tsx
@@ -15,4 +15,4 @@
*/
export { Box } from './Box';
export type * from './types';
-export { breakpoints, space, themes } from './properties';
+export { breakpoints, space, themes } from '../../layout/properties';
diff --git a/packages/canon/src/components/Box/sprinkles.css.ts b/packages/canon/src/components/Box/sprinkles.css.ts
index 36904cb2ab..df5664029b 100644
--- a/packages/canon/src/components/Box/sprinkles.css.ts
+++ b/packages/canon/src/components/Box/sprinkles.css.ts
@@ -14,16 +14,11 @@
* limitations under the License.
*/
-import {
- defineProperties,
- createSprinkles,
- RequiredConditionalValue,
- ConditionalValue,
- createMapValueFn,
-} from '@vanilla-extract/sprinkles';
-import { breakpoints, space } from './properties';
+import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
+import { breakpoints } from '../../layout/properties';
+import { colorProperties, spacingProperties } from '../../layout/sprinkles.css';
-export const responsiveProperties = defineProperties({
+export const boxProperties = defineProperties({
conditions: breakpoints,
defaultCondition: 'xs',
responsiveArray: ['xs', 'sm', 'md', 'lg', 'xl', '2xl'],
@@ -55,57 +50,12 @@ export const responsiveProperties = defineProperties({
error: '1px solid var(--canon-error)',
},
display: ['none', 'flex', 'block', 'inline'],
- paddingTop: space,
- paddingBottom: space,
- paddingLeft: space,
- paddingRight: space,
- marginTop: space,
- marginBottom: space,
- marginLeft: space,
- marginRight: space,
- gap: space,
flexWrap: ['wrap', 'nowrap'],
},
- shorthands: {
- padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
- paddingX: ['paddingLeft', 'paddingRight'],
- paddingY: ['paddingTop', 'paddingBottom'],
- margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
- marginX: ['marginLeft', 'marginRight'],
- marginY: ['marginTop', 'marginBottom'],
- },
-});
-
-export const colorProperties = defineProperties({
- conditions: {
- light: { selector: '[data-theme="light"] &' },
- dark: { selector: '[data-theme="dark"] &' },
- },
- defaultCondition: ['light', 'dark'],
- properties: {
- color: {
- primary: 'var(--canon-text-primary)',
- secondary: 'var(--canon-text-secondary)',
- error: 'var(--canon-error)',
- },
- background: {
- background: 'var(--canon-background)',
- elevation1: 'var(--canon-surface-1)',
- elevation2: 'var(--canon-surface-2)',
- transparent: 'transparent',
- },
- },
});
export const boxSprinkles = createSprinkles(
- responsiveProperties,
+ spacingProperties,
+ boxProperties,
colorProperties,
);
-
-export type OptionalResponsiveValue =
- ConditionalValue;
-
-export type RequiredResponsiveValue =
- RequiredConditionalValue;
-
-export const mapResponsiveValue = createMapValueFn(responsiveProperties);
diff --git a/packages/canon/src/components/Box/types.ts b/packages/canon/src/components/Box/types.ts
index 3f307bf2eb..2ba5929fdd 100644
--- a/packages/canon/src/components/Box/types.ts
+++ b/packages/canon/src/components/Box/types.ts
@@ -13,19 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { breakpoints, space, themes } from './properties';
+
+import { Breakpoint, SpaceProps, ColorProps } from '../../layout/types';
/** @public */
-export type Breakpoint = keyof typeof breakpoints;
-
-/** @public */
-export type Space = keyof typeof space;
-
-/** @public */
-export type Theme = keyof typeof themes;
-
-/** @public */
-export type Display =
+export type DisplayProps =
| 'flex'
| 'none'
| 'inline'
@@ -33,19 +25,19 @@ export type Display =
| Partial>;
/** @public */
-export type FlexDirection =
+export type FlexDirectionProps =
| 'row'
| 'column'
| Partial>;
/** @public */
-export type FlexWrap =
+export type FlexWrapProps =
| 'wrap'
| 'nowrap'
| Partial>;
/** @public */
-export type JustifyContent =
+export type JustifyContentProps =
| 'stretch'
| 'flex-start'
| 'center'
@@ -65,7 +57,7 @@ export type JustifyContent =
>;
/** @public */
-export type AlignItems =
+export type AlignItemsProps =
| 'stretch'
| 'flex-start'
| 'center'
@@ -75,7 +67,7 @@ export type AlignItems =
>;
/** @public */
-export type BorderRadius =
+export type BorderRadiusProps =
| 'none'
| 'small'
| 'medium'
@@ -83,94 +75,15 @@ export type BorderRadius =
| Partial>;
/** @public */
-export type Gap = Space | Partial>;
-
-/** @public */
-export type PaddingLeft = Space | Partial>;
-
-/** @public */
-export type PaddingRight = Space | Partial>;
-
-/** @public */
-export type PaddingTop = Space | Partial>;
-
-/** @public */
-export type PaddingBottom = Space | Partial>;
-
-/** @public */
-export type Padding = Space | Partial>;
-
-/** @public */
-export type PaddingX = Space | Partial>;
-
-/** @public */
-export type PaddingY = Space | Partial>;
-
-/** @public */
-export type MarginLeft = Space | Partial>;
-
-/** @public */
-export type MarginRight = Space | Partial>;
-
-/** @public */
-export type MarginTop = Space | Partial>;
-
-/** @public */
-export type MarginBottom = Space | Partial>;
-
-/** @public */
-export type Margin = Space | Partial>;
-
-/** @public */
-export type MarginX = Space | Partial>;
-
-/** @public */
-export type MarginY = Space | Partial>;
-
-/** @public */
-export type Background =
- | 'background'
- | 'elevation1'
- | 'elevation2'
- | 'transparent'
- | Partial<
- Record
- >;
-
-/** @public */
-export type Color =
- | 'primary'
- | 'secondary'
- | 'error'
- | Partial>;
-
-/** @public */
-export interface BoxProps {
+export interface BoxProps extends SpaceProps, ColorProps {
as?: keyof JSX.IntrinsicElements;
- background?: Background;
children?: React.ReactNode;
- color?: Color;
- display?: Display;
- flexDirection?: FlexDirection;
- flexWrap?: FlexWrap;
- justifyContent?: JustifyContent;
- alignItems?: AlignItems;
- borderRadius?: BorderRadius;
- gap?: Gap;
- padding?: Padding;
- paddingLeft?: PaddingLeft;
- paddingRight?: PaddingRight;
- paddingTop?: PaddingTop;
- paddingBottom?: PaddingBottom;
- paddingX?: PaddingX;
- paddingY?: PaddingY;
- margin?: Margin;
- marginLeft?: MarginLeft;
- marginRight?: MarginRight;
- marginTop?: MarginTop;
- marginBottom?: MarginBottom;
- marginX?: MarginX;
- marginY?: MarginY;
+ display?: DisplayProps;
+ flexDirection?: FlexDirectionProps;
+ flexWrap?: FlexWrapProps;
+ justifyContent?: JustifyContentProps;
+ alignItems?: AlignItemsProps;
+ borderRadius?: BorderRadiusProps;
className?: string;
style?: React.CSSProperties;
}
diff --git a/packages/canon/src/components/Inline/Inline.stories.tsx b/packages/canon/src/components/Inline/Inline.stories.tsx
index d098fa71a3..613fd68c68 100644
--- a/packages/canon/src/components/Inline/Inline.stories.tsx
+++ b/packages/canon/src/components/Inline/Inline.stories.tsx
@@ -18,38 +18,108 @@ import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Inline } from './Inline';
import { Box } from '../Box/Box';
+import { argTypesSpacing, argTypesColor } from '../../../docs/utils/argTypes';
const meta = {
title: 'Components/Inline',
component: Inline,
+ argTypes: {
+ ...argTypesSpacing,
+ ...argTypesColor,
+ align: {
+ control: 'inline-radio',
+ options: ['left', 'center', 'right'],
+ },
+ alignY: {
+ control: 'inline-radio',
+ options: ['top', 'center', 'bottom'],
+ },
+ children: {
+ control: false,
+ },
+ as: {
+ control: false,
+ },
+ className: {
+ control: 'text',
+ },
+ },
} satisfies Meta;
export default meta;
type Story = StoryObj;
-const FakeBox = () => (
+const FakeBox = ({
+ width = 120,
+ height = 80,
+}: {
+ width?: number;
+ height?: number;
+}) => (
- Fake Box
-
+ style={{ background: '#1f47ff', color: 'white', width, height }}
+ />
);
export const Default: Story = {
args: {
children: (
<>
-
-
-
+ {Array.from({ length: 32 }).map((_, index) => (
+
+ ))}
>
),
},
};
+export const AlignLeft: Story = {
+ args: {
+ ...Default.args,
+ align: 'left',
+ },
+};
+
+export const AlignCenter: Story = {
+ args: {
+ ...Default.args,
+ align: 'center',
+ },
+};
+
+export const AlignRight: Story = {
+ args: {
+ ...Default.args,
+ align: 'right',
+ },
+};
+
+export const VerticalAlignTop: Story = {
+ args: {
+ ...Default.args,
+ alignY: 'top',
+ },
+};
+
+export const VerticalAlignCenter: Story = {
+ args: {
+ ...Default.args,
+ alignY: 'center',
+ },
+};
+
+export const VerticalAlignBottom: Story = {
+ args: {
+ ...Default.args,
+ alignY: 'bottom',
+ },
+};
+
export const LargeGap: Story = {
args: {
...Default.args,
diff --git a/packages/canon/src/components/Inline/Inline.tsx b/packages/canon/src/components/Inline/Inline.tsx
index db9f4bd9c3..2d1c649e29 100644
--- a/packages/canon/src/components/Inline/Inline.tsx
+++ b/packages/canon/src/components/Inline/Inline.tsx
@@ -14,46 +14,50 @@
* limitations under the License.
*/
-import React from 'react';
-import { Box } from '../Box/Box';
-import { alignToFlexAlign } from '../../utils/align';
-import type { BoxProps } from '../Box/types';
+import { createElement } from 'react';
+import { inlineSprinkles } from './sprinkles.css';
+import type { InlineProps } from './types';
-export const validInlineComponents = [
- 'div',
- 'span',
- 'p',
- 'nav',
- 'ul',
- 'ol',
- 'li',
-] as const;
+const alignYToFlexAlign = (alignY: InlineProps['alignY']) => {
+ if (alignY === 'top') return 'flex-start';
+ if (alignY === 'center') return 'center';
+ if (alignY === 'bottom') return 'flex-end';
+ return undefined;
+};
-export interface InlineProps extends Omit {
- as?: (typeof validInlineComponents)[number];
- children: React.ReactNode;
- align?: 'left' | 'center' | 'right';
- gap?: BoxProps['gap'];
-}
+const alignToFlexAlignY = (align: InlineProps['align']) => {
+ if (align === 'left') return 'flex-start';
+ if (align === 'center') return 'center';
+ if (align === 'right') return 'flex-end';
+ return undefined;
+};
export const Inline = ({
- align,
as = 'div',
children,
+ align = 'left',
+ alignY = 'top',
gap = 'xs',
+ className,
+ style,
...restProps
}: InlineProps) => {
- return (
-
- {children}
-
- );
+ // Generate the list of class names
+ const sprinklesClassName = inlineSprinkles({
+ ...restProps,
+ gap,
+ alignItems: alignYToFlexAlign(alignY),
+ justifyContent: alignToFlexAlignY(align),
+ });
+
+ // Combine the base class name, the sprinkles class name, and any additional class names
+ const classNames = ['inline', sprinklesClassName, className]
+ .filter(Boolean)
+ .join(' ');
+
+ return createElement(as, {
+ className: classNames,
+ style,
+ children,
+ });
};
diff --git a/packages/canon/src/components/Inline/sprinkles.css.ts b/packages/canon/src/components/Inline/sprinkles.css.ts
new file mode 100644
index 0000000000..1d7e95da00
--- /dev/null
+++ b/packages/canon/src/components/Inline/sprinkles.css.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
+import { breakpoints } from '../../layout/properties';
+import { colorProperties, spacingProperties } from '../../layout/sprinkles.css';
+
+const inlineProperties = defineProperties({
+ conditions: breakpoints,
+ defaultCondition: 'xs',
+ properties: {
+ alignItems: ['flex-start', 'center', 'flex-end'],
+ justifyContent: ['flex-start', 'center', 'flex-end'],
+ },
+});
+
+export const inlineSprinkles = createSprinkles(
+ spacingProperties,
+ colorProperties,
+ inlineProperties,
+);
diff --git a/packages/canon/src/components/Inline/styles.css b/packages/canon/src/components/Inline/styles.css
new file mode 100644
index 0000000000..fb9b1c3825
--- /dev/null
+++ b/packages/canon/src/components/Inline/styles.css
@@ -0,0 +1,4 @@
+.inline {
+ display: flex;
+ flex-wrap: wrap;
+}
diff --git a/packages/canon/src/components/Inline/types.ts b/packages/canon/src/components/Inline/types.ts
new file mode 100644
index 0000000000..413f2fbbb0
--- /dev/null
+++ b/packages/canon/src/components/Inline/types.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AsProps, Breakpoint, ColorProps } from '../../layout/types';
+import { SpaceProps } from '../../layout/types';
+
+export interface InlineProps extends SpaceProps, ColorProps {
+ children: React.ReactNode;
+ as?: AsProps;
+ align?:
+ | 'left'
+ | 'center'
+ | 'right'
+ | Partial>;
+ alignY?:
+ | 'top'
+ | 'center'
+ | 'bottom'
+ | Partial>;
+ className?: string;
+ style?: React.CSSProperties;
+}
diff --git a/packages/canon/src/components/Stack/Stack.stories.tsx b/packages/canon/src/components/Stack/Stack.stories.tsx
index 9d01238ae7..601f6b847f 100644
--- a/packages/canon/src/components/Stack/Stack.stories.tsx
+++ b/packages/canon/src/components/Stack/Stack.stories.tsx
@@ -18,10 +18,32 @@ import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Stack } from './Stack';
import { Box } from '../Box/Box';
+import { argTypesSpacing, argTypesColor } from '../../../docs/utils/argTypes';
const meta = {
title: 'Components/Stack',
component: Stack,
+ argTypes: {
+ ...argTypesSpacing,
+ ...argTypesColor,
+ align: {
+ control: 'inline-radio',
+ options: ['left', 'center', 'right'],
+ },
+ children: {
+ control: false,
+ },
+ as: {
+ control: false,
+ },
+ className: {
+ control: 'text',
+ },
+ },
+ args: {
+ align: 'left',
+ gap: 'xs',
+ },
} satisfies Meta;
export default meta;
@@ -50,6 +72,49 @@ export const Default: Story = {
},
};
+export const AlignLeft: Story = {
+ args: {
+ ...Default.args,
+ align: 'left',
+ },
+};
+
+export const AlignCenter: Story = {
+ args: {
+ ...Default.args,
+ align: 'center',
+ },
+};
+
+export const AlignRight: Story = {
+ args: {
+ ...Default.args,
+ align: 'right',
+ },
+};
+
+export const ResponsiveAlign: Story = {
+ args: {
+ ...Default.args,
+ align: {
+ xs: 'left',
+ md: 'center',
+ lg: 'right',
+ },
+ },
+};
+
+export const ResponsiveGap: Story = {
+ args: {
+ ...Default.args,
+ gap: {
+ xs: 'xs',
+ md: 'md',
+ lg: 'xxl',
+ },
+ },
+};
+
export const LargeGap: Story = {
args: {
...Default.args,
diff --git a/packages/canon/src/components/Stack/Stack.tsx b/packages/canon/src/components/Stack/Stack.tsx
index 121ab1b176..f2316805b5 100644
--- a/packages/canon/src/components/Stack/Stack.tsx
+++ b/packages/canon/src/components/Stack/Stack.tsx
@@ -14,61 +14,44 @@
* limitations under the License.
*/
-import React from 'react';
-import { Box } from '../Box/Box';
-import { alignToFlexAlign } from '../../utils/align';
-import type { BoxProps } from '../Box/types';
-const validStackComponents = [
- 'div',
- 'span',
- 'p',
- 'article',
- 'section',
- 'main',
- 'nav',
- 'aside',
- 'ul',
- 'ol',
- 'li',
- 'details',
- 'summary',
- 'dd',
- 'dl',
- 'dt',
-] as const;
+import { createElement } from 'react';
+import { StackProps } from './types';
+import { stackSprinkles } from './sprinkles.css';
-export interface StackProps extends Omit {
- children: React.ReactNode;
- as?: (typeof validStackComponents)[number];
- align?: 'left' | 'center' | 'right';
- gap?: BoxProps['gap'];
-}
+const alignToFlexAlign = (align: StackProps['align']) => {
+ if (align === 'left') return 'stretch';
+ if (align === 'center') return 'center';
+ if (align === 'right') return 'flex-end';
+ return undefined;
+};
export const Stack = ({
as = 'div',
children,
- align: alignProp,
+ align = 'left',
gap = 'xs',
+ className,
+ style,
...restProps
}: StackProps) => {
- /**
- * Creating a seam between the provided prop and the default value
- * to enable only setting the text alignment when the `align` prop
- * is provided — not when it's defaulted.
- */
- const align = alignProp || 'left';
+ // Transform the align prop
+ const flexAlign = alignToFlexAlign(align);
- return (
-
- {children}
-
- );
+ // Generate the list of class names
+ const sprinklesClassName = stackSprinkles({
+ ...restProps,
+ gap,
+ alignItems: flexAlign,
+ });
+
+ // Combine the base class name, the sprinkles class name, and any additional class names
+ const classNames = ['stack', sprinklesClassName, className]
+ .filter(Boolean)
+ .join(' ');
+
+ return createElement(as, {
+ className: classNames,
+ style,
+ children,
+ });
};
diff --git a/packages/canon/src/components/Stack/sprinkles.css.ts b/packages/canon/src/components/Stack/sprinkles.css.ts
index 461f38fcde..85c8f83a71 100644
--- a/packages/canon/src/components/Stack/sprinkles.css.ts
+++ b/packages/canon/src/components/Stack/sprinkles.css.ts
@@ -13,74 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
-import { breakpoints, space } from '../Box/properties';
-import { colorProperties } from '../Box/sprinkles.css';
-const responsiveProperties = defineProperties({
+import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
+import { breakpoints } from '../../layout/properties';
+import { colorProperties, spacingProperties } from '../../layout/sprinkles.css';
+
+const stackProperties = defineProperties({
conditions: breakpoints,
defaultCondition: 'xs',
properties: {
- flexDirection: ['row', 'column'],
- justifyContent: [
- 'stretch',
- 'flex-start',
- 'center',
- 'flex-end',
- 'space-around',
- 'space-between',
- ],
alignItems: ['stretch', 'flex-start', 'center', 'flex-end'],
- borderRadius: {
- none: 0,
- small: '4px',
- medium: '8px',
- full: '9999px',
- },
- boxShadow: {
- small: 'var(--canon-box-shadow-small)',
- medium: 'var(--canon-box-shadow-medium)',
- large: 'var(--canon-box-shadow-large)',
- },
- border: {
- none: 'none',
- thin: '1px solid var(--canon-outline)',
- error: '1px solid var(--canon-error)',
- },
- display: ['none', 'flex', 'block', 'inline'],
- paddingTop: space,
- paddingBottom: space,
- paddingLeft: space,
- paddingRight: space,
- marginTop: space,
- marginBottom: space,
- marginLeft: space,
- marginRight: space,
- gap: space,
- flexWrap: ['wrap', 'nowrap'],
- },
- shorthands: {
- p: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
- pt: ['paddingTop'],
- pr: ['paddingRight'],
- pb: ['paddingBottom'],
- pl: ['paddingLeft'],
- px: ['paddingLeft', 'paddingRight'],
- py: ['paddingTop', 'paddingBottom'],
- m: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
- mt: ['marginTop'],
- mr: ['marginRight'],
- mb: ['marginBottom'],
- ml: ['marginLeft'],
- mx: ['marginLeft', 'marginRight'],
- my: ['marginTop', 'marginBottom'],
- direction: ['flexDirection'],
- items: ['alignItems'],
- justify: ['justifyContent'],
},
});
-export const sprinkles = createSprinkles(responsiveProperties, colorProperties);
-
-// It's a good idea to export the Sprinkles type too
-export type Sprinkles = Parameters[0];
+export const stackSprinkles = createSprinkles(
+ spacingProperties,
+ colorProperties,
+ stackProperties,
+);
diff --git a/packages/canon/src/components/Stack/styles.css b/packages/canon/src/components/Stack/styles.css
new file mode 100644
index 0000000000..784fd3b1ab
--- /dev/null
+++ b/packages/canon/src/components/Stack/styles.css
@@ -0,0 +1,4 @@
+.stack {
+ display: flex;
+ flex-direction: column;
+}
diff --git a/packages/canon/src/components/Stack/types.ts b/packages/canon/src/components/Stack/types.ts
new file mode 100644
index 0000000000..81abea9999
--- /dev/null
+++ b/packages/canon/src/components/Stack/types.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { AsProps, Breakpoint, ColorProps } from '../../layout/types';
+import { SpaceProps } from '../../layout/types';
+
+export interface StackProps extends SpaceProps, ColorProps {
+ children: React.ReactNode;
+ as?: AsProps;
+ align?:
+ | 'left'
+ | 'center'
+ | 'right'
+ | Partial>;
+ className?: string;
+ style?: React.CSSProperties;
+}
diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts
index 2bd3f11f48..198a626f06 100644
--- a/packages/canon/src/index.ts
+++ b/packages/canon/src/index.ts
@@ -23,3 +23,4 @@
export * from './components/Button';
export * from './components/Box';
export * from './components/Icon';
+export * from './layout/types';
diff --git a/packages/canon/src/components/Box/properties.ts b/packages/canon/src/layout/properties.ts
similarity index 100%
rename from packages/canon/src/components/Box/properties.ts
rename to packages/canon/src/layout/properties.ts
diff --git a/packages/canon/src/layout/sprinkles.css.ts b/packages/canon/src/layout/sprinkles.css.ts
new file mode 100644
index 0000000000..7e85487fe3
--- /dev/null
+++ b/packages/canon/src/layout/sprinkles.css.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { defineProperties } from '@vanilla-extract/sprinkles';
+import { breakpoints, space } from './properties';
+
+export const spacingProperties = defineProperties({
+ conditions: breakpoints,
+ defaultCondition: 'xs',
+ responsiveArray: ['xs', 'sm', 'md', 'lg', 'xl', '2xl'],
+ properties: {
+ paddingTop: space,
+ paddingBottom: space,
+ paddingLeft: space,
+ paddingRight: space,
+ marginTop: space,
+ marginBottom: space,
+ marginLeft: space,
+ marginRight: space,
+ gap: space,
+ },
+ shorthands: {
+ padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
+ paddingX: ['paddingLeft', 'paddingRight'],
+ paddingY: ['paddingTop', 'paddingBottom'],
+ margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
+ marginX: ['marginLeft', 'marginRight'],
+ marginY: ['marginTop', 'marginBottom'],
+ },
+});
+
+export const colorProperties = defineProperties({
+ conditions: {
+ light: { selector: '[data-theme="light"] &' },
+ dark: { selector: '[data-theme="dark"] &' },
+ },
+ defaultCondition: ['light', 'dark'],
+ properties: {
+ color: {
+ primary: 'var(--canon-text-primary)',
+ secondary: 'var(--canon-text-secondary)',
+ error: 'var(--canon-error)',
+ },
+ background: {
+ background: 'var(--canon-background)',
+ elevation1: 'var(--canon-surface-1)',
+ elevation2: 'var(--canon-surface-2)',
+ transparent: 'transparent',
+ },
+ },
+});
diff --git a/packages/canon/src/layout/types.ts b/packages/canon/src/layout/types.ts
new file mode 100644
index 0000000000..c051dacb44
--- /dev/null
+++ b/packages/canon/src/layout/types.ts
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2024 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { breakpoints, space, themes } from './properties';
+
+/** @public */
+export type Breakpoint = keyof typeof breakpoints;
+
+/** @public */
+export type Space = keyof typeof space;
+
+/** @public */
+export type Theme = keyof typeof themes;
+
+/** @public */
+export type Gap = Space | Partial>;
+
+/** @public */
+export type PaddingLeft = Space | Partial>;
+
+/** @public */
+export type PaddingRight = Space | Partial>;
+
+/** @public */
+export type PaddingTop = Space | Partial>;
+
+/** @public */
+export type PaddingBottom = Space | Partial>;
+
+/** @public */
+export type Padding = Space | Partial>;
+
+/** @public */
+export type PaddingX = Space | Partial>;
+
+/** @public */
+export type PaddingY = Space | Partial>;
+
+/** @public */
+export type MarginLeft = Space | Partial>;
+
+/** @public */
+export type MarginRight = Space | Partial>;
+
+/** @public */
+export type MarginTop = Space | Partial>;
+
+/** @public */
+export type MarginBottom = Space | Partial>;
+
+/** @public */
+export type Margin = Space | Partial>;
+
+/** @public */
+export type MarginX = Space | Partial>;
+
+/** @public */
+export type MarginY = Space | Partial>;
+
+/** @public */
+export interface SpaceProps {
+ gap?: Gap;
+ padding?: Padding;
+ paddingLeft?: PaddingLeft;
+ paddingRight?: PaddingRight;
+ paddingTop?: PaddingTop;
+ paddingBottom?: PaddingBottom;
+ paddingX?: PaddingX;
+ paddingY?: PaddingY;
+ margin?: Margin;
+ marginLeft?: MarginLeft;
+ marginRight?: MarginRight;
+ marginTop?: MarginTop;
+ marginBottom?: MarginBottom;
+ marginX?: MarginX;
+ marginY?: MarginY;
+}
+
+/** @public */
+export type Background =
+ | 'background'
+ | 'elevation1'
+ | 'elevation2'
+ | 'transparent'
+ | Partial<
+ Record
+ >;
+
+/** @public */
+export type Color =
+ | 'primary'
+ | 'secondary'
+ | 'error'
+ | Partial>;
+
+/** @public */
+export type AsProps =
+ | 'div'
+ | 'span'
+ | 'p'
+ | 'article'
+ | 'section'
+ | 'main'
+ | 'nav'
+ | 'aside'
+ | 'ul'
+ | 'ol'
+ | 'li'
+ | 'details'
+ | 'summary'
+ | 'dd'
+ | 'dl'
+ | 'dt';
+
+/** @public */
+export interface ColorProps {
+ color?: Color;
+ background?: Background;
+}
diff --git a/packages/canon/src/theme/styles.css b/packages/canon/src/theme/styles.css
index fa5ed2ba4d..4a31340575 100644
--- a/packages/canon/src/theme/styles.css
+++ b/packages/canon/src/theme/styles.css
@@ -23,3 +23,5 @@
/* Components */
@import '../components/Button/styles.css';
+@import '../components/Stack/styles.css';
+@import '../components/Inline/styles.css';
diff --git a/packages/canon/src/utils/align.ts b/packages/canon/src/utils/align.ts
deleted file mode 100644
index f4d7b5932e..0000000000
--- a/packages/canon/src/utils/align.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2024 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {
- type OptionalResponsiveValue,
- mapResponsiveValue,
-} from '../components/Box/sprinkles.css';
-
-export type Align = 'left' | 'center' | 'right';
-export type AlignY = 'top' | 'center' | 'bottom';
-
-const alignToFlexAlignLookup = {
- left: 'flex-start',
- center: 'center',
- right: 'flex-end',
-} as const;
-
-export const alignToFlexAlign = (
- align: OptionalResponsiveValue | undefined,
-) =>
- align
- ? mapResponsiveValue(align, value => alignToFlexAlignLookup[value])
- : undefined;
-
-const alignYToFlexAlignLookup = {
- top: 'flex-start',
- center: 'center',
- bottom: 'flex-end',
-} as const;
-
-export const alignYToFlexAlign = (
- alignY: OptionalResponsiveValue | undefined,
-) =>
- alignY
- ? mapResponsiveValue(alignY, value => alignYToFlexAlignLookup[value])
- : undefined;