Improve layout components

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2024-11-23 22:29:21 +00:00
parent cc7a706683
commit 080d676a7b
15 changed files with 532 additions and 451 deletions
+14 -197
View File
@@ -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';
<Meta of={BoxStories} />
@@ -20,137 +17,19 @@ import {
<Title type="h1">Box</Title>
<Text>
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 `<div>` element and
provides a consistent API for styling and layout. You'll find below a list of all the available properties.
</Text>
<Stack
direction="column"
items="center"
justify="center"
borderRadius="small"
mb="xxl"
gap="xl"
py="xl"
style={{ border: '1px solid #e7e7e7' }}
>
<Stack items="center" gap="xl">
<Stack
items="center"
justify="center"
borderRadius="small"
p="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<Stack
px="md"
py="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
<span>padding</span>
</Stack>
</Stack>
<Stack
items="center"
justify="center"
borderRadius="small"
px="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<Stack
px="md"
py="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
<span>paddingX</span>
</Stack>
</Stack>
<Stack
items="center"
justify="center"
borderRadius="small"
py="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<Stack
px="md"
py="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
<span>paddingY</span>
</Stack>
</Stack>
</Stack>
<Stack items="center" gap="xl">
<Stack
items="center"
justify="center"
borderRadius="small"
pt="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<Stack
px="md"
py="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
<span>paddingTop</span>
</Stack>
</Stack>
<Stack
items="center"
justify="center"
borderRadius="small"
pb="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<Stack
px="md"
py="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
<span>paddingBottom</span>
</Stack>
</Stack>
<Stack
items="center"
justify="center"
borderRadius="small"
pl="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<Stack
px="md"
py="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
<span>paddingLeft</span>
</Stack>
</Stack>
<Stack
items="center"
justify="center"
borderRadius="small"
pr="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<Stack
px="md"
py="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
<span>paddingRight</span>
</Stack>
</Stack>
</Stack>
</Stack>
<PropsTable />
<Title type="h2">Padding & Margin</Title>
<Text>
Padding and margin are used to create space around your component using our
predefined spacing tokens. We would recommend to use padding over margin to
avoid collapsing margins but both are available.
</Text>
<Padding />
<Table.Root>
<Table.Header>
<Table.HeaderRow>
@@ -160,68 +39,6 @@ import {
</Table.HeaderRow>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>alignItems</Chip>
</Table.Cell>
<Table.Cell>
{alignItems.map(n => (
<Chip>{n}</Chip>
))}
</Table.Cell>
<Table.Cell>
<Chip>stretch</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>background</Chip>
</Table.Cell>
<Table.Cell>
{Object.keys(backgroundColors).map(n => (
<Chip>{n}</Chip>
))}
</Table.Cell>
<Table.Cell>
<Chip>transparent</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>borderRadius</Chip>
</Table.Cell>
<Table.Cell>
{Object.keys(borderRadius).map(n => (
<Chip>{n}</Chip>
))}
</Table.Cell>
<Table.Cell>
<Chip>none</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>boxShadow</Chip>
</Table.Cell>
<Table.Cell>
{Object.keys(boxShadows).map(n => (
<Chip>{n}</Chip>
))}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>display</Chip>
</Table.Cell>
<Table.Cell>
{display.map(n => (
<Chip>{n}</Chip>
))}
</Table.Cell>
<Table.Cell>
<Chip>block</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>padding</Chip>
@@ -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<typeof meta>;
export const Basic: Story = {
render: () => (
<Box
padding="medium"
padding="xs"
background="background"
paddingX="large"
paddingY="medium"
paddingX="lg"
paddingY="md"
borderRadius="small"
>
Basic Box
@@ -47,7 +48,7 @@ export const Responsive: Story = {
render: () => (
<Box
display={{ xs: 'block', sm: 'flex' }}
padding={{ xs: 'small', sm: 'medium', lg: 'large' }}
padding={{ xs: 'xs', sm: 'md', lg: 'lg' }}
background="background"
>
Responsive Box
@@ -60,16 +61,16 @@ export const FlexContainer: Story = {
<Box
display="flex"
flexDirection="column"
padding="large"
padding="lg"
background="background"
>
<Box padding="medium" background="background">
<Box padding="md" background="background" color="primary">
Item 1
</Box>
<Box padding="medium" background="background">
<Box padding="md" background="background" color="primary">
Item 2
</Box>
<Box padding="medium" background="background">
<Box padding="md" background="background" color="primary">
Item 3
</Box>
</Box>
@@ -78,24 +79,24 @@ export const FlexContainer: Story = {
export const Nested: Story = {
render: () => (
<Box padding="large" background="background">
<Box padding="medium" background="background">
<Box padding="lg" background="background">
<Box padding="md" background="background">
Header
</Box>
<Box
display="flex"
padding="medium"
padding="md"
background="background"
justifyContent="space-between"
>
<Box padding="small" background="background">
<Box padding="xs" background="background">
Sidebar
</Box>
<Box padding="small" background="background">
<Box padding="xs" background="background">
Main Content
</Box>
</Box>
<Box padding="medium" background="background">
<Box padding="md" background="background">
Footer
</Box>
</Box>
@@ -106,13 +107,13 @@ export const Alignment: Story = {
render: () => (
<Box
display="flex"
padding="large"
padding="lg"
background="background"
justifyContent="center"
alignItems="center"
style={{ height: '200px' }}
>
<Box padding="medium" background="background">
<Box padding="md" background="background">
Centered Content
</Box>
</Box>
@@ -122,17 +123,17 @@ export const Alignment: Story = {
// Example showing different spacing combinations
export const Spacing: Story = {
render: () => (
<Box display="flex" flexDirection="column" gap="medium">
<Box padding="small" background="background">
<Box display="flex" flexDirection="column" gap="md">
<Box padding="xs" background="background">
Small Padding
</Box>
<Box padding="medium" background="background">
<Box padding="md" background="background">
Medium Padding
</Box>
<Box padding="large" background="background">
<Box padding="lg" background="background">
Large Padding
</Box>
<Box paddingX="large" paddingY="small" background="background">
<Box paddingX="lg" paddingY="xs" background="background">
Mixed Padding
</Box>
</Box>
@@ -142,14 +143,14 @@ export const Spacing: Story = {
// Example showing different display values
export const DisplayVariants: Story = {
render: () => (
<Box display="flex" flexDirection="column" gap="medium">
<Box padding="medium" background="background" display="block">
<Box display="flex" flexDirection="column" gap="md">
<Box padding="md" background="background" display="block">
Display Block
</Box>
<Box padding="medium" background="background" display="flex">
<Box padding="md" background="background" display="flex">
Display Flex
</Box>
<Box padding="medium" background="background" display="inline">
<Box padding="md" background="background" display="inline">
Display Inline
</Box>
</Box>
@@ -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 }) => (
<Box
px="md"
py="sm"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
{children}
</Box>
);
export const Padding = () => {
return (
<Stack
align="center"
borderRadius="small"
mb="md"
gap="xl"
py="xl"
style={{ border: '1px solid #e7e7e7' }}
>
<Inline align="center" gap="xl">
<Stack
align="center"
borderRadius="small"
p="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>padding</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
px="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingX</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
py="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingY</FakeBox>
</Stack>
</Inline>
<Inline align="center" gap="xl">
<Stack
align="center"
borderRadius="small"
pt="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingTop</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
pb="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingBottom</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
pl="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingLeft</FakeBox>
</Stack>
<Stack
align="center"
borderRadius="small"
pr="md"
style={{ background: '#c4cafb', color: 'white' }}
>
<FakeBox>paddingRight</FakeBox>
</Stack>
</Inline>
</Stack>
);
};
@@ -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 (
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Prop</Table.HeaderCell>
<Table.HeaderCell>Type</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
{Object.keys(responsiveProperties.styles)
.filter(
n =>
![
'padding',
'paddingX',
'paddingY',
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'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 => (
<Table.Row key={n}>
<Table.Cell>
<Chip head>{n}</Chip>
</Table.Cell>
<Table.Cell>
{listResponsiveValues(
n as keyof typeof responsiveProperties.styles,
).map(value => (
<Chip key={value}>{value}</Chip>
))}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};
@@ -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)',
@@ -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<string, string[]> = {
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<typeof sprinkles>[0];
export type OptionalResponsiveValue<Value extends string | number> =
ConditionalValue<typeof responsiveProperties, Value>;
export type RequiredResponsiveValue<Value extends string | number> =
RequiredConditionalValue<typeof responsiveProperties, Value>;
export const mapResponsiveValue = createMapValueFn(responsiveProperties);
@@ -0,0 +1,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<typeof Inline>;
export default meta;
type Story = StoryObj<typeof meta>;
const FakeBox = () => (
<Box
px="xl"
py="md"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
Fake Box
</Box>
);
export const Default: Story = {
args: {
children: (
<>
<FakeBox />
<FakeBox />
<FakeBox />
</>
),
},
};
export const LargeGap: Story = {
args: {
...Default.args,
gap: 'xl',
},
};
@@ -0,0 +1,58 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { Box, BoxProps } from '../box/box';
import { alignToFlexAlign } from '../../utils/align';
export const validInlineComponents = [
'div',
'span',
'p',
'nav',
'ul',
'ol',
'li',
] as const;
export interface InlineProps extends Omit<BoxProps, 'alignItems'> {
as?: (typeof validInlineComponents)[number];
children: React.ReactNode;
align?: 'left' | 'center' | 'right';
gap?: BoxProps['gap'];
}
export const Inline = ({
align,
as = 'div',
children,
gap = 'xs',
...restProps
}: InlineProps) => {
return (
<Box
as={as}
display="flex"
alignItems={align !== 'left' ? alignToFlexAlign(align) : undefined}
justifyContent="flex-start"
flexWrap="wrap"
gap={gap}
{...restProps}
>
{children}
</Box>
);
};
@@ -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<typeof sprinkles>[0];
@@ -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',
});
@@ -21,42 +21,37 @@ import { Box } from '../box/box';
const meta = {
title: 'Layout/Stack',
component: Stack,
parameters: {
layout: 'centered',
},
// tags: ['autodocs'],
} satisfies Meta<typeof Stack>;
export default meta;
type Story = StoryObj<typeof meta>;
const FakeBox = () => (
<Box
background="background"
paddingX="large"
paddingY="medium"
px="xl"
py="md"
borderRadius="small"
style={{ background: '#1f47ff', color: 'white' }}
>
Fake Box
</Box>
);
export default meta;
type Story = StoryObj<typeof meta>;
export const Column: Story = {
render: () => (
<Stack gap="medium" direction="column">
<FakeBox />
<FakeBox />
<FakeBox />
</Stack>
),
export const Default: Story = {
args: {
children: (
<>
<FakeBox />
<FakeBox />
<FakeBox />
</>
),
},
};
export const Row: Story = {
render: () => (
<Stack gap="medium" direction="row">
<FakeBox />
<FakeBox />
<FakeBox />
</Stack>
),
export const LargeGap: Story = {
args: {
...Default.args,
gap: 'xl',
},
};
+51 -36
View File
@@ -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<HTMLElement>,
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<BoxProps, 'alignItems'> {
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<string, unknown> = {};
const nativeProps: Record<string, unknown> = {};
/**
* 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 (
<Box
as={as}
display="flex"
flexDirection="column"
alignItems={align !== 'left' ? alignToFlexAlign(align) : undefined}
gap={gap}
// textAlign={alignProp}
{...restProps}
>
{children}
</Box>
);
};
+2 -2
View File
@@ -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;
+33
View File
@@ -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<Align> | undefined,
) =>
align
? mapResponsiveValue(align, value => alignToFlexAlignLookup[value])
: undefined;
const alignYToFlexAlignLookup = {
top: 'flex-start',
center: 'center',
bottom: 'flex-end',
} as const;
export const alignYToFlexAlign = (
alignY: OptionalResponsiveValue<AlignY> | undefined,
) =>
alignY
? mapResponsiveValue(alignY, value => alignYToFlexAlignLookup[value])
: undefined;
+26
View File
@@ -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 [];
};