diff --git a/packages/canon/src/components/box/box.mdx b/packages/canon/src/components/box/box.mdx index e827d8fcfd..f8cadc316b 100644 --- a/packages/canon/src/components/box/box.mdx +++ b/packages/canon/src/components/box/box.mdx @@ -5,13 +5,10 @@ import * as Table from '../../storybook/table'; import { Box } from './box'; import { Stack } from '../stack/stack'; import { Title, Text } from '../../storybook'; -import { - alignItems, - backgroundColors, - boxShadows, - borderRadius, - display, -} from './properties'; +import { Padding } from './docs/padding'; +import { listResponsiveValues, listColorValues } from '../../utils/list-values'; +import { responsiveProperties } from './sprinkles.css'; +import { PropsTable } from './docs/props-table'; @@ -20,137 +17,19 @@ import { Box Box is the lowest-level component in Canon. We use it internally to build all - of our components. + of our components. It is a wrapper around the HTML `
` element and + provides a consistent API for styling and layout. You'll find below a list of all the available properties. - - - - - padding - - - - - paddingX - - - - - paddingY - - - - - - - paddingTop - - - - - paddingBottom - - - - - paddingLeft - - - - - paddingRight - - - - + +Padding & Margin + + 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. + + @@ -160,68 +39,6 @@ import { - - - alignItems - - - {alignItems.map(n => ( - {n} - ))} - - - stretch - - - - - background - - - {Object.keys(backgroundColors).map(n => ( - {n} - ))} - - - transparent - - - - - borderRadius - - - {Object.keys(borderRadius).map(n => ( - {n} - ))} - - - none - - - - - boxShadow - - - {Object.keys(boxShadows).map(n => ( - {n} - ))} - - - - - display - - - {display.map(n => ( - {n} - ))} - - - block - - padding diff --git a/packages/canon/src/components/box/box.stories.tsx b/packages/canon/src/components/box/box.stories.tsx index fa78813a73..e85c2d1eb9 100644 --- a/packages/canon/src/components/box/box.stories.tsx +++ b/packages/canon/src/components/box/box.stories.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { Box } from './box'; @@ -32,10 +33,10 @@ type Story = StoryObj; export const Basic: Story = { render: () => ( Basic Box @@ -47,7 +48,7 @@ export const Responsive: Story = { render: () => ( Responsive Box @@ -60,16 +61,16 @@ export const FlexContainer: Story = { - + Item 1 - + Item 2 - + Item 3 @@ -78,24 +79,24 @@ export const FlexContainer: Story = { export const Nested: Story = { render: () => ( - - + + Header - + Sidebar - + Main Content - + Footer @@ -106,13 +107,13 @@ export const Alignment: Story = { render: () => ( - + Centered Content @@ -122,17 +123,17 @@ export const Alignment: Story = { // Example showing different spacing combinations export const Spacing: Story = { render: () => ( - - + + Small Padding - + Medium Padding - + Large Padding - + Mixed Padding @@ -142,14 +143,14 @@ export const Spacing: Story = { // Example showing different display values export const DisplayVariants: Story = { render: () => ( - - + + Display Block - + Display Flex - + Display Inline diff --git a/packages/canon/src/components/box/docs/padding.tsx b/packages/canon/src/components/box/docs/padding.tsx new file mode 100644 index 0000000000..189846c39a --- /dev/null +++ b/packages/canon/src/components/box/docs/padding.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Stack } from '../../stack/stack'; +import { Inline } from '../../inline/inline'; +import { Box } from '../box'; + +const FakeBox = ({ children }: { children: string }) => ( + + {children} + +); + +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 new file mode 100644 index 0000000000..de4de09bfe --- /dev/null +++ b/packages/canon/src/components/box/docs/props-table.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import * as Table from '../../../storybook'; +import { Chip } from '../../../storybook'; +import { listResponsiveValues } from '../../../utils/list-values'; +import { responsiveProperties } from '../sprinkles.css'; + +export const PropsTable = () => { + return ( + + + + Prop + Type + + + + {Object.keys(responsiveProperties.styles) + .filter( + n => + ![ + 'padding', + 'paddingX', + 'paddingY', + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'paddingBottom', + 'p', + 'px', + 'py', + 'pl', + 'pr', + 'pt', + 'pb', + 'margin', + 'marginX', + 'marginY', + 'marginLeft', + 'marginRight', + 'marginTop', + 'marginBottom', + 'm', + 'mx', + 'my', + 'ml', + 'mr', + 'mt', + 'mb', + ].includes(n), + ) + .map(n => ( + + + {n} + + + {listResponsiveValues( + n as keyof typeof responsiveProperties.styles, + ).map(value => ( + {value} + ))} + + + ))} + + + ); +}; diff --git a/packages/canon/src/components/box/properties.ts b/packages/canon/src/components/box/properties.ts index 5d4e851a0e..9063aa24d6 100644 --- a/packages/canon/src/components/box/properties.ts +++ b/packages/canon/src/components/box/properties.ts @@ -1,5 +1,3 @@ -export const alignItems = ['stretch', 'flex-start', 'center', 'flex-end']; - export const breakpoints = { xs: {}, sm: { '@media': 'screen and (min-width: 640px)' }, @@ -9,49 +7,6 @@ export const breakpoints = { '2xl': { '@media': 'screen and (min-width: 1536px)' }, }; -export const borderRadius = { - none: 0, - small: '4px', - medium: '8px', - full: '9999px', -}; - -export const backgroundColors = { - background: 'var(--canon-color-background)', - elevation1: 'var(--canon-color-elevation1)', - elevation2: 'var(--canon-color-elevation2)', - transparent: 'transparent', -}; - -export const border = { - none: 'none', - thin: '1px solid var(--canon-outline)', - error: '1px solid var(--canon-error)', -}; - -export const boxShadows = { - small: 'var(--canon-box-shadow-small)', - medium: 'var(--canon-box-shadow-medium)', - large: 'var(--canon-box-shadow-large)', -}; - -export const colors = { - background: '#eff6ff', -}; - -export const display = ['none', 'flex', 'block', 'inline']; - -export const flexDirection = ['row', 'column'] as const; - -export const justifyContent = [ - 'stretch', - 'flex-start', - 'center', - 'flex-end', - 'space-around', - 'space-between', -]; - export const space = { none: 0, xxs: 'var(--space-xxs)', diff --git a/packages/canon/src/components/box/sprinkles.css.ts b/packages/canon/src/components/box/sprinkles.css.ts index e8e912fef4..e8bcc8af15 100644 --- a/packages/canon/src/components/box/sprinkles.css.ts +++ b/packages/canon/src/components/box/sprinkles.css.ts @@ -13,84 +13,101 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles'; + import { - alignItems, - borderRadius, - breakpoints, - colors, - backgroundColors, - display, - flexDirection, - justifyContent, - space, - boxShadows, - border, -} from './properties'; + defineProperties, + createSprinkles, + RequiredConditionalValue, + ConditionalValue, + createMapValueFn, +} from '@vanilla-extract/sprinkles'; +import { breakpoints, space } from './properties'; -export const commonProperties = { - flexDirection, - justifyContent, - alignItems, - borderRadius, - boxShadow: boxShadows, - border, - paddingTop: space, - paddingBottom: space, - paddingLeft: space, - paddingRight: space, - marginTop: space, - marginBottom: space, - marginLeft: space, - marginRight: space, - gap: space, -}; - -export const boxShorthands: Record = { - padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], - paddingX: ['paddingLeft', 'paddingRight'], - paddingY: ['paddingTop', 'paddingBottom'], - p: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], - pt: ['paddingTop'], - pr: ['paddingRight'], - pb: ['paddingBottom'], - pl: ['paddingLeft'], - px: ['paddingLeft', 'paddingRight'], - py: ['paddingTop', 'paddingBottom'], - margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'], - marginX: ['marginLeft', 'marginRight'], - marginY: ['marginTop', 'marginBottom'], - m: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'], - mt: ['marginTop'], - mr: ['marginRight'], - mb: ['marginBottom'], - ml: ['marginLeft'], - mx: ['marginLeft', 'marginRight'], - my: ['marginTop', 'marginBottom'], -}; - -const responsiveProperties = defineProperties({ +export const responsiveProperties = defineProperties({ conditions: breakpoints, defaultCondition: 'xs', + responsiveArray: ['xs', 'sm', 'md', 'lg', 'xl', '2xl'], properties: { - ...commonProperties, - display, + 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: { - ...boxShorthands, - placeItems: ['justifyContent', 'alignItems'], + padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], + paddingX: ['paddingLeft', 'paddingRight'], + paddingY: ['paddingTop', 'paddingBottom'], + p: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'], + pt: ['paddingTop'], + pr: ['paddingRight'], + pb: ['paddingBottom'], + pl: ['paddingLeft'], + px: ['paddingLeft', 'paddingRight'], + py: ['paddingTop', 'paddingBottom'], + margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'], + marginX: ['marginLeft', 'marginRight'], + marginY: ['marginTop', 'marginBottom'], + m: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'], + mt: ['marginTop'], + mr: ['marginRight'], + mb: ['marginBottom'], + ml: ['marginLeft'], + mx: ['marginLeft', 'marginRight'], + my: ['marginTop', 'marginBottom'], }, }); -const colorProperties = defineProperties({ +export const colorProperties = defineProperties({ conditions: { light: { selector: '[data-theme="light"] &' }, dark: { selector: '[data-theme="dark"] &' }, }, defaultCondition: ['light', 'dark'], properties: { - color: colors, - background: backgroundColors, + 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', + }, }, }); @@ -98,3 +115,11 @@ export const sprinkles = createSprinkles(responsiveProperties, colorProperties); // It's a good idea to export the Sprinkles type too export type Sprinkles = Parameters[0]; + +export type OptionalResponsiveValue = + ConditionalValue; + +export type RequiredResponsiveValue = + RequiredConditionalValue; + +export const mapResponsiveValue = createMapValueFn(responsiveProperties); diff --git a/packages/canon/src/components/inline/inline.stories.tsx b/packages/canon/src/components/inline/inline.stories.tsx new file mode 100644 index 0000000000..9562a0b914 --- /dev/null +++ b/packages/canon/src/components/inline/inline.stories.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Inline } from './inline'; +import { Box } from '../box/box'; + +const meta = { + title: 'Layout/Inline', + component: Inline, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const FakeBox = () => ( + + Fake Box + +); + +export const Default: Story = { + args: { + children: ( + <> + + + + + ), + }, +}; + +export const LargeGap: Story = { + args: { + ...Default.args, + gap: 'xl', + }, +}; diff --git a/packages/canon/src/components/inline/inline.tsx b/packages/canon/src/components/inline/inline.tsx new file mode 100644 index 0000000000..0da15303f9 --- /dev/null +++ b/packages/canon/src/components/inline/inline.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Box, BoxProps } from '../box/box'; +import { alignToFlexAlign } from '../../utils/align'; + +export const validInlineComponents = [ + 'div', + 'span', + 'p', + 'nav', + 'ul', + 'ol', + 'li', +] as const; + +export interface InlineProps extends Omit { + as?: (typeof validInlineComponents)[number]; + children: React.ReactNode; + align?: 'left' | 'center' | 'right'; + gap?: BoxProps['gap']; +} + +export const Inline = ({ + align, + as = 'div', + children, + gap = 'xs', + ...restProps +}: InlineProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/canon/src/components/stack/sprinkles.css.ts b/packages/canon/src/components/stack/sprinkles.css.ts deleted file mode 100644 index 6a300a4044..0000000000 --- a/packages/canon/src/components/stack/sprinkles.css.ts +++ /dev/null @@ -1,37 +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 { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles'; -import { breakpoints } from '../box/properties'; -import { boxShorthands, commonProperties } from '../box/sprinkles.css'; - -const responsiveProperties = defineProperties({ - conditions: breakpoints, - defaultCondition: 'xs', - properties: { - ...commonProperties, - }, - shorthands: { - ...boxShorthands, - direction: ['flexDirection'], - items: ['alignItems'], - justify: ['justifyContent'], - }, -}); - -export const sprinkles = createSprinkles(responsiveProperties); - -// It's a good idea to export the Sprinkles type too -export type Sprinkles = Parameters[0]; diff --git a/packages/canon/src/components/stack/stack.css.ts b/packages/canon/src/components/stack/stack.css.ts deleted file mode 100644 index 3ede7d94e4..0000000000 --- a/packages/canon/src/components/stack/stack.css.ts +++ /dev/null @@ -1,21 +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 { style } from '@vanilla-extract/css'; - -export const stackStyles = style({ - fontFamily: 'var(--canon-font-sans)', - display: 'flex', -}); diff --git a/packages/canon/src/components/stack/stack.stories.tsx b/packages/canon/src/components/stack/stack.stories.tsx index e60de21312..209811880a 100644 --- a/packages/canon/src/components/stack/stack.stories.tsx +++ b/packages/canon/src/components/stack/stack.stories.tsx @@ -21,42 +21,37 @@ import { Box } from '../box/box'; const meta = { title: 'Layout/Stack', component: Stack, - parameters: { - layout: 'centered', - }, - // tags: ['autodocs'], } satisfies Meta; +export default meta; +type Story = StoryObj; + const FakeBox = () => ( Fake Box ); -export default meta; -type Story = StoryObj; - -export const Column: Story = { - render: () => ( - - - - - - ), +export const Default: Story = { + args: { + children: ( + <> + + + + + ), + }, }; -export const Row: Story = { - render: () => ( - - - - - - ), +export const LargeGap: Story = { + args: { + ...Default.args, + gap: 'xl', + }, }; diff --git a/packages/canon/src/components/stack/stack.tsx b/packages/canon/src/components/stack/stack.tsx index b9b160e01b..90ea6c0502 100644 --- a/packages/canon/src/components/stack/stack.tsx +++ b/packages/canon/src/components/stack/stack.tsx @@ -13,47 +13,62 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createElement } from 'react'; -import { sprinkles, Sprinkles } from './sprinkles.css'; -import { stackStyles } from './stack.css'; -type HTMLProperties = Omit< - React.AllHTMLAttributes, - keyof Sprinkles ->; +import React from 'react'; +import { Box, BoxProps } from '../box/box'; +import { alignToFlexAlign } from '../../utils/align'; -export type StackProps = Sprinkles & - HTMLProperties & { - as?: keyof JSX.IntrinsicElements; - }; +const validStackComponents = [ + 'div', + 'span', + 'p', + 'article', + 'section', + 'main', + 'nav', + 'aside', + 'ul', + 'ol', + 'li', + 'details', + 'summary', + 'dd', + 'dl', + 'dt', +] as const; + +export interface StackProps extends Omit { + children: React.ReactNode; + as?: (typeof validStackComponents)[number]; + align?: 'left' | 'center' | 'right'; + gap?: BoxProps['gap']; +} export const Stack = ({ as = 'div', - className, - style, - ...props + children, + align: alignProp, + gap = 'xs', + ...restProps }: StackProps) => { - const sprinklesProps: Record = {}; - const nativeProps: Record = {}; + /** + * 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'; - // Split props between sprinkles and native HTML props - Object.entries(props).forEach(([key, value]) => { - if (value === undefined) return; - - if (sprinkles.properties.has(key as keyof Sprinkles)) { - sprinklesProps[key] = value; - } else { - nativeProps[key] = value; - } - }); - - const sprinklesClassName = sprinkles(sprinklesProps); - - return createElement(as, { - className: [stackStyles, sprinklesClassName, className] - .filter(Boolean) - .join(' '), - style, - ...nativeProps, - }); + return ( + + {children} + + ); }; diff --git a/packages/canon/src/theme/theme.css b/packages/canon/src/theme/theme.css index ec69322d0d..1633a83fc0 100644 --- a/packages/canon/src/theme/theme.css +++ b/packages/canon/src/theme/theme.css @@ -1,7 +1,7 @@ /* Light theme */ :root { --canon-accent: #1ed760; - --canon-bg: #fff; + --canon-background: #fff; --canon-surface-1: #f5f5f5; --canon-surface-2: #000; --canon-outline: #666; @@ -31,7 +31,7 @@ /* Dark theme */ [data-theme='dark'] { --canon-accent: #1ed760; - --canon-bg: #fff; + --canon-background: #fff; --canon-surface-1: #f5f5f5; --canon-surface-2: #000; --canon-outline: #666; diff --git a/packages/canon/src/utils/align.ts b/packages/canon/src/utils/align.ts new file mode 100644 index 0000000000..bee9b35e01 --- /dev/null +++ b/packages/canon/src/utils/align.ts @@ -0,0 +1,33 @@ +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; diff --git a/packages/canon/src/utils/list-values.ts b/packages/canon/src/utils/list-values.ts new file mode 100644 index 0000000000..31ccc7e0ed --- /dev/null +++ b/packages/canon/src/utils/list-values.ts @@ -0,0 +1,26 @@ +import { + responsiveProperties, + colorProperties, +} from '../components/box/sprinkles.css'; + +export const listResponsiveValues = ( + value: keyof typeof responsiveProperties.styles, +) => { + const values = responsiveProperties.styles[value]; + + if ('values' in values) { + return Object.keys(values.values); + } + + return []; +}; + +export const listColorValues = (value: keyof typeof colorProperties.styles) => { + const values = colorProperties.styles[value]; + + if ('values' in values) { + return Object.keys(values.values); + } + + return []; +};