Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion code/ui/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"@storybook/client-logger": "workspace:*",
"@storybook/csf": "^0.1.0",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.1.5",
"@storybook/icons": "^1.1.6",
"@storybook/theming": "workspace:*",
"@storybook/types": "workspace:*",
"memoizerific": "^1.11.3",
Expand Down
100 changes: 37 additions & 63 deletions code/ui/components/src/new/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { Icon } from '@storybook/components/experimental';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
Expand All @@ -17,110 +15,86 @@ export const Base = {
args: { children: 'Button' },
};

export const Types: Story = {
export const Variants: Story = {
args: {
...Base.args,
},
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="solid">Solid</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};

export const Active: Story = {
render: () => (
args: {
...Base.args,
},
render: (args) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button variant="primary" active>
Primary
<Button variant="solid" active {...args}>
Solid
</Button>
<Button variant="secondary" active>
Secondary
<Button variant="outline" active {...args}>
Outline
</Button>
<Button variant="tertiary" active>
Tertiary
<Button variant="ghost" active {...args}>
Ghost
</Button>
</div>
),
};

export const WithIcon: Story = {
render: () => (
args: {
...Base.args,
icon: 'FaceHappy',
},
render: ({ icon, children }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button variant="primary" icon={<Icon.FaceHappy />}>
Primary
<Button variant="solid" icon={icon}>
{children}
</Button>
<Button variant="secondary" icon={<Icon.FaceHappy />}>
Secondary
<Button variant="outline" icon={icon}>
{children}
</Button>
<Button variant="tertiary" icon={<Icon.FaceHappy />}>
Tertiary
<Button variant="ghost" icon={icon}>
{children}
</Button>
</div>
),
};

export const Sizes: Story = {
args: {
...Base.args,
},
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button size="small" icon={<Icon.FaceHappy />}>
<Button size="small" icon="FaceHappy">
Small Button
</Button>
<Button size="small" icon={<Icon.FaceHappy />} iconOnly />
<Button size="medium" icon={<Icon.FaceHappy />}>
<Button size="medium" icon="FaceHappy">
Medium Button
</Button>
<Button size="medium" icon={<Icon.FaceHappy />} iconOnly />
</div>
),
};

export const IconOnly: Story = {
parameters: {
docs: {
description: {
story: 'This is a story that shows how to use the `iconOnly` prop.',
},
source: {
type: 'dynamic',
},
},
},
render: () => (
<>
<Button size="small" variant="primary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="small" variant="secondary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="small" variant="tertiary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="medium" variant="primary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="medium" variant="secondary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="medium" variant="tertiary" iconOnly icon={<Icon.FaceHappy />} />
</>
),
decorators: [
(Story) => <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>{Story()}</div>,
],
};

export const IconOnlyActive: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button size="small" variant="primary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="small" variant="secondary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="small" variant="tertiary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="medium" variant="primary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="medium" variant="secondary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="medium" variant="tertiary" iconOnly icon={<Icon.FaceHappy />} active />
</div>
),
};

export const Disabled: Story = {
args: {
...Base.args,
disabled: true,
children: 'Disabled Button',
},
};

export const WithHref: Story = {
args: {
...Base.args,
},
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button onClick={() => console.log('Hello')}>I am a button using onClick</Button>
Expand Down
80 changes: 29 additions & 51 deletions code/ui/components/src/new/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,34 @@
import type { ReactNode } from 'react';
import React, { forwardRef } from 'react';
import { styled } from '@storybook/theming';
import { darken, lighten, rgba, transparentize } from 'polished';
import type { Icons } from '@storybook/icons';
import type { PropsOf } from '../utils/types';
import { Icon } from '../Icon/Icon';

interface CommonProps<T extends React.ElementType = React.ElementType> {
interface ButtonProps<T extends React.ElementType = React.ElementType> {
children: string;
as?: T;
size?: 'small' | 'medium';
variant?: 'primary' | 'secondary' | 'tertiary';
variant?: 'solid' | 'outline' | 'ghost';
onClick?: () => void;
disabled?: boolean;
active?: boolean;
icon?: Icons;
}

type ButtonIconOnlyProps = {
iconOnly: true;
icon: ReactNode;
children?: never;
};

type ButtonWithTextProps = {
iconOnly?: false;
icon?: ReactNode;
children: string;
};

type ButtonProps<T extends React.ElementType = React.ElementType> = CommonProps<T> &
(ButtonIconOnlyProps | ButtonWithTextProps);

export const Button: {
<E extends React.ElementType = 'button'>(
props: ButtonProps<E> & Omit<PropsOf<E>, keyof ButtonProps>
): JSX.Element;
displayName?: string;
} = forwardRef(
({ as, children, icon, ...props }: ButtonProps, ref: React.Ref<HTMLButtonElement>) => {
const LocalIcon = Icon[icon];

return (
<StyledButton as={as} ref={ref} {...props}>
{icon}
{!props.iconOnly && children}
{icon && <LocalIcon />}
Comment thread
kasperpeulen marked this conversation as resolved.
{children}
</StyledButton>
);
}
Expand All @@ -47,14 +37,7 @@ export const Button: {
Button.displayName = 'Button';

const StyledButton = styled.button<Omit<ButtonProps, 'children'>>(
({
theme,
variant = 'primary',
size = 'medium',
disabled = false,
active = false,
iconOnly = false,
}) => ({
({ theme, variant = 'solid', size = 'medium', disabled = false, active = false }) => ({
border: 0,
cursor: disabled ? 'not-allowed' : 'pointer',
display: 'inline-flex',
Expand All @@ -63,15 +46,10 @@ const StyledButton = styled.button<Omit<ButtonProps, 'children'>>(
justifyContent: 'center',
overflow: 'hidden',
padding: `${(() => {
if (!iconOnly && size === 'small') return '0 10px';
if (!iconOnly && size === 'medium') return '0 12px';
if (size === 'small') return '0 10px';
if (size === 'medium') return '0 12px';
return 0;
})()}`,
width: `${(() => {
if (iconOnly && size === 'small') return '28px';
if (iconOnly && size === 'medium') return '32px';
return 'auto';
})()}`,
height: size === 'small' ? '28px' : '32px',
position: 'relative',
textAlign: 'center',
Expand All @@ -88,41 +66,41 @@ const StyledButton = styled.button<Omit<ButtonProps, 'children'>>(
fontWeight: theme.typography.weight.bold,
lineHeight: '1',
background: `${(() => {
if (variant === 'primary') return theme.color.secondary;
if (variant === 'secondary') return theme.button.background;
if (variant === 'tertiary' && active) return theme.background.hoverable;
if (variant === 'solid') return theme.color.secondary;
if (variant === 'outline') return theme.button.background;
if (variant === 'ghost' && active) return theme.background.hoverable;
return 'transparent';
})()}`,
color: `${(() => {
if (variant === 'primary') return theme.color.lightest;
if (variant === 'secondary') return theme.input.color;
if (variant === 'tertiary' && active) return theme.color.secondary;
if (variant === 'tertiary') return theme.color.mediumdark;
if (variant === 'solid') return theme.color.lightest;
if (variant === 'outline') return theme.input.color;
if (variant === 'ghost' && active) return theme.color.secondary;
if (variant === 'ghost') return theme.color.mediumdark;
return theme.input.color;
})()}`,
boxShadow: variant === 'secondary' ? `${theme.button.border} 0 0 0 1px inset` : 'none',
boxShadow: variant === 'outline' ? `${theme.button.border} 0 0 0 1px inset` : 'none',
borderRadius: theme.input.borderRadius,

'&:hover': {
color: variant === 'tertiary' ? theme.color.secondary : null,
color: variant === 'ghost' ? theme.color.secondary : null,
background: `${(() => {
let bgColor = theme.color.secondary;
if (variant === 'primary') bgColor = theme.color.secondary;
if (variant === 'secondary') bgColor = theme.button.background;
if (variant === 'solid') bgColor = theme.color.secondary;
if (variant === 'outline') bgColor = theme.button.background;

if (variant === 'tertiary') return transparentize(0.86, theme.color.secondary);
if (variant === 'ghost') return transparentize(0.86, theme.color.secondary);
return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor);
})()}`,
},

'&:active': {
color: variant === 'tertiary' ? theme.color.secondary : null,
color: variant === 'ghost' ? theme.color.secondary : null,
background: `${(() => {
let bgColor = theme.color.secondary;
if (variant === 'primary') bgColor = theme.color.secondary;
if (variant === 'secondary') bgColor = theme.button.background;
if (variant === 'solid') bgColor = theme.color.secondary;
if (variant === 'outline') bgColor = theme.button.background;

if (variant === 'tertiary') return theme.background.hoverable;
if (variant === 'ghost') return theme.background.hoverable;
return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor);
})()}`,
},
Expand Down
62 changes: 62 additions & 0 deletions code/ui/components/src/new/IconButton/IconButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { IconButton } from './IconButton';

const meta: Meta<typeof IconButton> = {
title: 'IconButton',
component: IconButton,
tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof IconButton>;

export const Base = {
args: { icon: 'FaceHappy' },
};

export const Types: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton variant="solid" icon="FaceHappy" />
<IconButton variant="outline" icon="FaceHappy" />
<IconButton variant="ghost" icon="FaceHappy" />
</div>
),
};

export const Active: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton variant="solid" icon="FaceHappy" active />
<IconButton variant="outline" icon="FaceHappy" active />
<IconButton variant="ghost" icon="FaceHappy" active />
</div>
),
};

export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton size="small" icon="FaceHappy" />
<IconButton size="medium" icon="FaceHappy" />
</div>
),
};

export const Disabled: Story = {
args: {
...Base.args,
icon: 'FaceHappy',
disabled: true,
},
};

export const WithHref: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton icon="FaceHappy" onClick={() => console.log('Hello')} />
<IconButton as="a" href="https://storybook.js.org/" icon="FaceHappy" />
</div>
),
};
Loading