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
5 changes: 5 additions & 0 deletions .changeset/lovely-bushes-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shipfox/react-ui": minor
---

Add Button Link and Icon Button components
32 changes: 29 additions & 3 deletions libs/react/ui/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,17 @@
--background-button-transparent-default: var(--color-alpha-white-0);
--background-button-neutral-hover: var(--color-neutral-100);
--background-button-danger-hover: var(--color-red-700);
--background-button-success-hover: var(--color-green-700);
--background-button-neutral-pressed: var(--color-neutral-200);
--background-button-neutral-default: var(--color-neutral-0);
--background-button-transparent-hover: var(--color-alpha-white-6);
--background-button-transparent-hover: var(--color-alpha-black-6);
--background-button-inverted-pressed: var(--color-neutral-600);
--background-button-transparent-pressed: var(--color-alpha-white-10);
--background-button-transparent-pressed: var(--color-alpha-black-10);
--background-button-danger-pressed: var(--color-red-800);
--background-button-success-pressed: var(--color-green-800);
--background-button-inverted-hover: var(--color-neutral-700);
--background-button-danger-default: var(--color-red-600);
--background-button-success-default: var(--color-green-600);
--background-button-inverted-default: var(--color-neutral-800);

/* Accent Background */
Expand Down Expand Up @@ -312,7 +315,7 @@
var(--color-alpha-black-8);
--shadow-button-neutral-focus:
0 -1px 0 0 var(--color-alpha-white-6), 0 0 0 1px var(--color-alpha-white-6), 0 0 0 1px
var(--color-neutral-800), 0 0 0 2px var(--color-alpha-white-88), 0 0 0 4px
var(--color-alpha-black-8), 0 0 0 2px var(--color-alpha-white-88), 0 0 0 4px
var(--color-primary-500);
--shadow-button-danger:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
Expand All @@ -321,6 +324,14 @@
--shadow-button-danger-focus:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
var(--color-red-700), 0 0 0 2px var(--color-alpha-white-88), 0 0 0 4px var(--color-primary-500);
--shadow-button-success:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
var(--color-green-700), 0 0 1px 1.5px var(--color-alpha-black-24), 0 2px 2px 0
var(--color-alpha-black-24);
--shadow-button-success-focus:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
var(--color-green-700), 0 0 0 2px var(--color-alpha-white-88), 0 0 0 4px
var(--color-primary-500);

/* Checkbox */
/* Checkbox - Unchecked States */
Expand Down Expand Up @@ -392,14 +403,17 @@
--background-button-transparent-default: var(--color-alpha-white-0);
--background-button-neutral-hover: var(--color-alpha-white-8);
--background-button-danger-hover: var(--color-red-700);
--background-button-success-hover: var(--color-green-700);
--background-button-neutral-pressed: var(--color-alpha-white-12);
--background-button-neutral-default: var(--color-alpha-white-4);
--background-button-transparent-hover: var(--color-alpha-white-8);
--background-button-inverted-pressed: var(--color-neutral-400);
--background-button-transparent-pressed: var(--color-alpha-white-12);
--background-button-danger-pressed: var(--color-red-600);
--background-button-success-pressed: var(--color-green-600);
--background-button-inverted-hover: var(--color-neutral-500);
--background-button-danger-default: var(--color-red-800);
--background-button-success-default: var(--color-green-800);
--background-button-inverted-default: var(--color-neutral-600);

/* Accent Background */
Expand Down Expand Up @@ -513,6 +527,13 @@
--shadow-button-danger-focus:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
var(--color-red-700), 0 0 0 2px var(--color-neutral-950), 0 0 0 4px var(--color-primary-500);
--shadow-button-success:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
var(--color-green-700), 0 0 1px 1.5px var(--color-alpha-black-24), 0 2px 2px 0
var(--color-alpha-black-24);
--shadow-button-success-focus:
0 -1px 0 0 var(--color-alpha-white-16), 0 0 0 1px var(--color-alpha-white-12), 0 0 0 1px
var(--color-green-700), 0 0 0 2px var(--color-neutral-950), 0 0 0 4px var(--color-primary-500);

/* Checkbox */
/* Checkbox - Unchecked States */
Expand Down Expand Up @@ -727,14 +748,17 @@
--color-background-button-transparent-default: var(--background-button-transparent-default);
--color-background-button-neutral-hover: var(--background-button-neutral-hover);
--color-background-button-danger-hover: var(--background-button-danger-hover);
--color-background-button-success-hover: var(--background-button-success-hover);
--color-background-button-neutral-pressed: var(--background-button-neutral-pressed);
--color-background-button-neutral-default: var(--background-button-neutral-default);
--color-background-button-transparent-hover: var(--background-button-transparent-hover);
--color-background-button-inverted-pressed: var(--background-button-inverted-pressed);
--color-background-button-transparent-pressed: var(--background-button-transparent-pressed);
--color-background-button-danger-pressed: var(--background-button-danger-pressed);
--color-background-button-success-pressed: var(--background-button-success-pressed);
--color-background-button-inverted-hover: var(--background-button-inverted-hover);
--color-background-button-danger-default: var(--background-button-danger-default);
--color-background-button-success-default: var(--background-button-success-default);
--color-background-button-inverted-default: var(--background-button-inverted-default);

/* Theme tokens - accent background */
Expand Down Expand Up @@ -875,6 +899,8 @@
--shadow-button-neutral-focus: var(--shadow-button-neutral-focus);
--shadow-button-danger: var(--shadow-button-danger);
--shadow-button-danger-focus: var(--shadow-button-danger-focus);
--shadow-button-success: var(--shadow-button-success);
--shadow-button-success-focus: var(--shadow-button-success-focus);

/* Checkbox */
--color-checkbox-unchecked-bg: var(--checkbox-unchecked-bg);
Expand Down
86 changes: 86 additions & 0 deletions libs/react/ui/src/components/button/button-link.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type {Meta, StoryObj} from '@storybook/react';
import {ButtonLink} from './button-link';

const meta = {
title: 'Components/Button/ButtonLink',
component: ButtonLink,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['base', 'interactive', 'muted', 'subtle'],
},
size: {
control: 'select',
options: ['xs', 'sm', 'md', 'xl'],
},
underline: {control: 'boolean'},
asChild: {control: 'boolean'},
},
args: {
children: 'Label',
variant: 'base',
size: 'sm',
underline: false,
href: '#',
},
} satisfies Meta<typeof ButtonLink>;

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

export const Default: Story = {};

export const Variants: Story = {
render: (args) => (
<div className="flex gap-16 items-center">
<ButtonLink {...args} variant="base">
Base
</ButtonLink>
<ButtonLink {...args} variant="interactive">
Interactive
</ButtonLink>
<ButtonLink {...args} variant="muted">
Muted
</ButtonLink>
<ButtonLink {...args} variant="subtle">
Subtle
</ButtonLink>
</div>
),
};

export const WithUnderline: Story = {
render: (args) => (
<div className="flex gap-16 items-center">
<ButtonLink {...args} variant="base" underline>
Base
</ButtonLink>
<ButtonLink {...args} variant="interactive" underline>
Interactive
</ButtonLink>
<ButtonLink {...args} variant="muted" underline>
Muted
</ButtonLink>
<ButtonLink {...args} variant="subtle" underline>
Subtle
</ButtonLink>
</div>
),
};

export const WithIcons: Story = {
render: (args) => (
<div className="flex gap-16 items-center">
<ButtonLink {...args} iconLeft="addLine">
Icon Left
</ButtonLink>
<ButtonLink {...args} iconRight="chevronRight">
Icon Right
</ButtonLink>
<ButtonLink {...args} iconLeft="addLine" iconRight="chevronRight">
Both Icons
</ButtonLink>
</div>
),
};
76 changes: 76 additions & 0 deletions libs/react/ui/src/components/button/button-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {Slot} from '@radix-ui/react-slot';
import {cva, type VariantProps} from 'class-variance-authority';
import {Icon, type IconName} from 'components/icon';
import type {ComponentProps} from 'react';
import {cn} from 'utils/cn';

export const buttonLinkVariants = cva(
'inline-flex items-center justify-center gap-4 whitespace-nowrap transition-colors disabled:pointer-events-none outline-none font-medium',
{
variants: {
variant: {
base: 'text-foreground-neutral-base hover:text-foreground-neutral-base focus-visible:text-foreground-neutral-base disabled:text-foreground-neutral-disabled',
interactive:
'text-foreground-highlight-interactive hover:text-foreground-highlight-interactive-hover focus-visible:text-foreground-highlight-interactive disabled:text-foreground-neutral-disabled',
muted:
'text-foreground-neutral-muted hover:text-foreground-neutral-base focus-visible:text-foreground-neutral-base disabled:text-foreground-neutral-disabled',
subtle:
'text-foreground-neutral-subtle hover:text-foreground-neutral-base focus-visible:text-foreground-neutral-base disabled:text-foreground-neutral-disabled',
},
size: {
xs: 'text-xs',
sm: 'text-sm',
md: 'text-md',
xl: 'text-xl',
},
underline: {
true: 'underline decoration-solid [text-underline-position:from-font]',
false: '',
},
},
defaultVariants: {
variant: 'base',
size: 'sm',
underline: false,
},
},
);

const iconSizeMap = {
xs: 14,
sm: 14,
md: 16,
xl: 20,
} as const;

export function ButtonLink({
className,
variant,
size = 'sm',
underline,
asChild = false,
children,
iconLeft,
iconRight,
...props
}: ComponentProps<'a'> &
VariantProps<typeof buttonLinkVariants> & {
asChild?: boolean;
iconLeft?: IconName;
iconRight?: IconName;
}) {
const Comp = asChild ? Slot : 'a';
const iconSize = iconSizeMap[size as keyof typeof iconSizeMap];

return (
<Comp
data-slot="button-link"
className={cn(buttonLinkVariants({variant, size, underline, className}))}
{...props}
>
{iconLeft && <Icon name={iconLeft} size={iconSize} />}
{children}
{iconRight && <Icon name={iconRight} size={iconSize} />}
</Comp>
);
}
8 changes: 1 addition & 7 deletions libs/react/ui/src/components/button/button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const variantOptions = [
'primary',
'secondary',
'danger',
'success',
'transparent',
'transparentMuted',
] as const;
Expand Down Expand Up @@ -48,7 +49,6 @@ export const Variants: Story = {
<th>{size}</th>
<th>Default</th>
<th>Hover</th>
<th>Active</th>
<th>Focus</th>
<th>Disabled</th>
</tr>
Expand All @@ -71,11 +71,6 @@ export const Variants: Story = {
Click me
</Button>
</td>
<td>
<Button {...args} variant={variant} className="active" size={size}>
Click me
</Button>
</td>
<td>
<Button {...args} variant={variant} className="focus" size={size}>
Click me
Expand All @@ -98,7 +93,6 @@ export const Variants: Story = {
Variants.parameters = {
pseudo: {
hover: '.hover',
active: '.active',
focusVisible: '.focus',
},
};
Expand Down
14 changes: 8 additions & 6 deletions libs/react/ui/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@ export const buttonVariants = cva(
'bg-background-button-neutral-default text-foreground-neutral-base shadow-button-neutral hover:bg-background-button-neutral-hover active:bg-background-button-neutral-pressed disabled:bg-background-neutral-disabled focus-visible:shadow-button-neutral-focus disabled:text-foreground-neutral-disabled disabled:shadow-none',
danger:
'bg-background-button-danger-default text-foreground-neutral-on-color shadow-button-danger hover:bg-background-button-danger-hover active:bg-background-button-danger-pressed focus-visible:shadow-button-danger-focus disabled:bg-background-neutral-disabled disabled:text-foreground-neutral-disabled disabled:shadow-none',
success:
'bg-background-button-success-default text-foreground-neutral-on-color shadow-button-success hover:bg-background-button-success-hover active:bg-background-button-success-pressed focus-visible:shadow-button-success-focus disabled:bg-background-neutral-disabled disabled:text-foreground-neutral-disabled disabled:shadow-none',
transparent:
'bg-background-button-transparent-default text-foreground-neutral-base hover:bg-background-button-transparent-hover active:bg-background-button-transparent-pressed focus-visible:shadow-button-neutral-focus disabled:text-foreground-neutral-disabled',
transparentMuted:
'bg-background-button-transparent-default text-foreground-neutral-muted hover:bg-background-button-transparent-hover active:bg-background-button-transparent-pressed focus-visible:shadow-button-neutral-focus disabled:text-foreground-neutral-disabled',
},
size: {
'2xs': 'px-6 text-xs gap-4',
xs: 'px-6 py-2 text-xs gap-4',
sm: 'px-8 py-4 text-sm gap-6',
md: 'px-10 py-6 text-md gap-8',
lg: 'px-12 py-8 text-lg gap-8',
xl: 'px-12 py-10 text-xl gap-10',
'2xs': 'h-20 px-6 text-xs gap-4',
xs: 'h-24 px-6 text-xs gap-4',
sm: 'h-28 px-8 text-sm gap-6',
md: 'h-32 px-10 text-md gap-8',
lg: 'h-36 px-12 text-lg gap-8',
xl: 'h-40 px-12 text-xl gap-10',
},
},
defaultVariants: {
Expand Down
Loading