diff --git a/.changeset/lovely-bushes-relax.md b/.changeset/lovely-bushes-relax.md new file mode 100644 index 00000000..be90932b --- /dev/null +++ b/.changeset/lovely-bushes-relax.md @@ -0,0 +1,5 @@ +--- +"@shipfox/react-ui": minor +--- + +Add Button Link and Icon Button components diff --git a/libs/react/ui/index.css b/libs/react/ui/index.css index 57857035..8e3c1ecb 100644 --- a/libs/react/ui/index.css +++ b/libs/react/ui/index.css @@ -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 */ @@ -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 @@ -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 */ @@ -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 */ @@ -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 */ @@ -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 */ @@ -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); diff --git a/libs/react/ui/src/components/button/button-link.stories.tsx b/libs/react/ui/src/components/button/button-link.stories.tsx new file mode 100644 index 00000000..a8ab1e83 --- /dev/null +++ b/libs/react/ui/src/components/button/button-link.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Variants: Story = { + render: (args) => ( +
+ + Base + + + Interactive + + + Muted + + + Subtle + +
+ ), +}; + +export const WithUnderline: Story = { + render: (args) => ( +
+ + Base + + + Interactive + + + Muted + + + Subtle + +
+ ), +}; + +export const WithIcons: Story = { + render: (args) => ( +
+ + Icon Left + + + Icon Right + + + Both Icons + +
+ ), +}; diff --git a/libs/react/ui/src/components/button/button-link.tsx b/libs/react/ui/src/components/button/button-link.tsx new file mode 100644 index 00000000..27747809 --- /dev/null +++ b/libs/react/ui/src/components/button/button-link.tsx @@ -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 & { + asChild?: boolean; + iconLeft?: IconName; + iconRight?: IconName; + }) { + const Comp = asChild ? Slot : 'a'; + const iconSize = iconSizeMap[size as keyof typeof iconSizeMap]; + + return ( + + {iconLeft && } + {children} + {iconRight && } + + ); +} diff --git a/libs/react/ui/src/components/button/button.stories.tsx b/libs/react/ui/src/components/button/button.stories.tsx index d1ce4e2f..7404a927 100644 --- a/libs/react/ui/src/components/button/button.stories.tsx +++ b/libs/react/ui/src/components/button/button.stories.tsx @@ -6,6 +6,7 @@ const variantOptions = [ 'primary', 'secondary', 'danger', + 'success', 'transparent', 'transparentMuted', ] as const; @@ -48,7 +49,6 @@ export const Variants: Story = { {size} Default Hover - Active Focus Disabled @@ -71,11 +71,6 @@ export const Variants: Story = { Click me - - -