Skip to content

Commit

Permalink
Build out new UI components (#1489)
Browse files Browse the repository at this point in the history
* Build new components

* Fix quotation marks

* Move colors to other PR

* Delete button-related components for now

* Split out typography components

* Add docs for Detail

* Document typography file

* Fix import

* Fix a few issues with how Storybook displays the Grid story

* Revert "Split out typography components"

This reverts commit 7740d3f.

* Create a Text component

* Create Storybook stories for Penumbra colors (#1500)
  • Loading branch information
jessepinho authored Jul 21, 2024
1 parent 6d64273 commit 11d1d4e
Show file tree
Hide file tree
Showing 11 changed files with 898 additions and 1 deletion.
10 changes: 10 additions & 0 deletions packages/ui/src/Colors.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as ColorsStories from './Colors.stories';

<Meta title='Colors' />

# Colors

Below are the colors we use at Penumbra. We deliberately do not include RGB/hex values for our colors, as they should be used via tokens — e.g., `text.primary`, `neutral.light`, `destructive.main`, etc. — to ensure consistency.

<Canvas of={ColorsStories.ColorGrid} />
108 changes: 108 additions & 0 deletions packages/ui/src/Colors.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Grid } from './Grid';
import { Technical } from './Typography';
import styled from 'styled-components';
import {
type ColorVariant,
type Color as TColor,
type TextColorVariant,
} from './ThemeProvider/theme';
import { Fragment } from 'react';
import { media } from './utils/media';

const meta: Meta = {};
export default meta;

const Label = styled.div`
display: flex;
height: 100%;
${media.tablet`
align-items: center;
`}
`;

const Variants = styled.div`
display: grid;
gap: ${props => props.theme.spacing(4)};
grid-template-columns: 1fr;
${media.tablet`
grid-template-columns: repeat(4, 1fr);
`}
`;

type VariantProps =
| {
$color: 'text';
$colorVariant: TextColorVariant;
}
| {
$color: Exclude<TColor, 'text' | 'action' | 'other'>;
$colorVariant: ColorVariant;
};

const Variant = styled.div<VariantProps>`
background-color: ${props =>
props.$color === 'text' ? 'transparent' : props.theme.color[props.$color][props.$colorVariant]};
border-radius: ${props => props.theme.borderRadius.xl};
color: ${props =>
props.$color === 'text'
? props.theme.color.text[props.$colorVariant]
: props.$colorVariant === 'contrast' || props.$colorVariant === 'light'
? props.theme.color[props.$color].dark
: props.theme.color.text.primary};
padding: ${props => props.theme.spacing(2)};
`;

const BASE_COLORS: Exclude<TColor, 'text' | 'action' | 'other'>[] = [
'neutral',
'primary',
'secondary',
'unshield',
'destructive',
'caution',
'success',
];

const Color = <T extends Exclude<TColor, 'action' | 'other'>>({ color }: { color: T }) => (
<Fragment key={color}>
<Grid mobile={6} tablet={2}>
<Label>
<Technical>{color}</Technical>
</Label>
</Grid>
<Grid mobile={6} tablet={10}>
<Variants>
{color === 'text'
? (['primary', 'secondary', 'disabled', 'special'] as const).map(variant => (
<Variant key={variant} $color={color} $colorVariant={variant}>
<Technical>{variant}</Technical>
</Variant>
))
: (['main', 'light', 'dark', 'contrast'] as const).map(variant => (
<Variant key={variant} $color={color} $colorVariant={variant}>
<Technical>{variant}</Technical>
</Variant>
))}
</Variants>
</Grid>
</Fragment>
);

export const ColorGrid: StoryObj = {
tags: ['!dev'],
render: function Render() {
return (
<>
<Grid container as='section'>
<Color color='text' />

{BASE_COLORS.map(color => (
<Color key={color} color={color} />
))}
</Grid>
</>
);
},
};
74 changes: 74 additions & 0 deletions packages/ui/src/Grid/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Grid } from '.';
import styled from 'styled-components';
import { Text } from '../Text';

const meta: Meta<typeof Grid> = {
component: Grid,
title: 'Grid',
tags: ['autodocs', '!dev'],
argTypes: {
container: { control: false },
mobile: { control: false },
tablet: { control: false },
desktop: { control: false },
lg: { control: false },
xl: { control: false },
as: { control: false },
},
};
export default meta;

type Story = StoryObj<typeof Grid>;

const Item = styled.div`
background-color: ${props => props.theme.color.neutral.main};
display: flex;
align-items: center;
justify-content: center;
padding: ${props => props.theme.spacing(2)};
`;

export const Demo: Story = {
render: function Render() {
return (
<Grid container as='main'>
<Grid mobile={12} as='section'>
<Item>
<Text technical>mobile=12</Text>
</Item>
</Grid>

{Array(2)
.fill(null)
.map((_, index) => (
<Grid mobile={12} tablet={6} key={index}>
<Item>
<Text technical>mobile=12 tablet=6</Text>
</Item>
</Grid>
))}

{Array(4)
.fill(null)
.map((_, index) => (
<Grid mobile={6} tablet={6} desktop={3} key={index}>
<Item>
<Text technical>mobile=6 tablet=6 desktop=3</Text>
</Item>
</Grid>
))}

{Array(48)
.fill(null)
.map((_, index) => (
<Grid key={index} lg={1}>
<Item>
<Text technical>lg=1</Text>
</Item>
</Grid>
))}
</Grid>
);
},
};
135 changes: 135 additions & 0 deletions packages/ui/src/Grid/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { PropsWithChildren } from 'react';
import styled from 'styled-components';
import { AsTransientProps, asTransientProps } from '../utils/asTransientProps';
import { media } from '../utils/media';

type GridElement = 'div' | 'main' | 'section';

interface BaseGridProps extends Record<string, unknown> {
/** Which element to use. Defaults to `'div'`. */
as?: GridElement;
}

interface GridContainerProps extends BaseGridProps {
/** Whether this is a grid container, vs. an item. */
container: true;

// For some reason, Storybook needs these properties to be defined on the
// container props interface in order to show their typings properly.
mobile?: undefined;
tablet?: undefined;
desktop?: undefined;
lg?: undefined;
xl?: undefined;
}

interface GridItemProps extends BaseGridProps {
/** Whether this is a grid container, vs. an item. */
container?: false;
/**
* The number of columns this grid item should span on mobile.
*
* The mobile grid layout can only be split in half, so you can only set a
* grid item to 6 or 12 columns on mobile.
*/
mobile?: 6 | 12;
/**
* The number of columns this grid item should span on tablet.
*
* The tablet grid layout can only be split into six columns.
*/
tablet?: 2 | 4 | 6 | 8 | 10 | 12;
/** The number of columns this grid item should span on desktop. */
desktop?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
/** The number of columns this grid item should span on large screens. */
lg?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
/** The number of columns this grid item should span on XL screens. */
xl?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
}

export type GridProps = PropsWithChildren<GridContainerProps | GridItemProps>;

const Container = styled.div`
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: ${props => props.theme.spacing(4)};
`;

const Item = styled.div<AsTransientProps<Exclude<GridItemProps, 'container'>>>`
${props => media.mobile`
grid-column: span ${props.$mobile ?? 12};
`}
${props =>
props.$tablet &&
media.tablet`
grid-column: span ${props.$tablet};
`}
${props =>
props.$desktop &&
media.desktop`
grid-column: span ${props.$desktop};
`}
${props =>
props.$lg &&
media.lg`
grid-column: span ${props.$lg};
`}
${props =>
props.$xl &&
media.xl`
grid-column: span ${props.$xl};
`}
`;

/**
* A responsive grid component that makes 12-column layouts super easy to build.
*
* Pass the `container` prop to the root `<Grid />` component; then, any nested
* children `<Grid />`s will be treated as grid items. You can customize which
* HTML element to use for each grid container or item by passing the element's
* name via the optional `as` prop.
*
* Use the `<Grid />` component — rather than styling your own HTML elements
* with `display: grid` — to ensure consistent behavior (such as grid gutters)
* throughout your app.
*
* ```tsx
* <Grid container as="main">
* <Grid mobile={12} as="section">This will span the full width on all screen sizes.</Grid>
*
* <Grid>So will this.</Grid>
*
* <Grid mobile={12} desktop={6}>
* These will span the full width on mobile...
* </Grid>
*
* <Grid mobile={12} desktop={6}>
* ...but half the width on desktop.
* </Grid>
*
* <Grid mobile={4}>
* These will...
* </Grid>
*
* <Grid mobile={4}>
* ...take up...
* </Grid>
*
* <Grid mobile={4}>
* ...one third each.
* </Grid>
* </Grid>
* ```
*/
export const Grid = ({ container, children, as = 'div', ...props }: GridProps) =>
container ? (
<Container as={as}>{children}</Container>
) : (
<Item {...asTransientProps(props)} as={as}>
{children}
</Item>
);
2 changes: 1 addition & 1 deletion packages/ui/src/Icon/index.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Icon } from '.';

const meta: Meta<typeof Icon> = {
component: Icon,
tags: ['autodocs'],
tags: ['autodocs', '!dev'],
argTypes: {
IconComponent: {
options: ['ArrowRightLeft', 'Send', 'Wallet'],
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/Icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const PROPS_BY_SIZE: Record<IconSize, ComponentProps<LucideIcon>> = {
* component rather than rendering Lucide icon components directly, since this
* component standardizes the stroke width and sizes throughout the Penumbra
* ecosystem.
*
* ```tsx
* <Icon IconComponent={ArrowRightLeft} size='sm' color={theme.colors.primary.main} />
* ```
*/
export const Icon = ({ IconComponent, size = 'sm', color }: IconProps) => (
<IconComponent absoluteStrokeWidth {...PROPS_BY_SIZE[size]} color={color} />
Expand Down
Loading

0 comments on commit 11d1d4e

Please sign in to comment.