From 671c6281179b5aa572d4964dc11fb24bbb0565ef Mon Sep 17 00:00:00 2001 From: Yehoshua Sandler Date: Wed, 16 Apr 2025 10:33:46 -0500 Subject: [PATCH] feat: added catalyst-ui-kit components --- src/components/alert.tsx | 95 +++++++++++++ src/components/auth-layout.tsx | 11 ++ src/components/avatar.tsx | 84 ++++++++++++ src/components/badge.tsx | 82 +++++++++++ src/components/button.tsx | 204 ++++++++++++++++++++++++++++ src/components/checkbox.tsx | 157 +++++++++++++++++++++ src/components/combobox.tsx | 188 +++++++++++++++++++++++++ src/components/description-list.tsx | 37 +++++ src/components/dialog.tsx | 86 ++++++++++++ src/components/divider.tsx | 20 +++ src/components/dropdown.tsx | 188 +++++++++++++++++++++++++ src/components/fieldset.tsx | 91 +++++++++++++ src/components/heading.tsx | 27 ++++ src/components/input.tsx | 94 +++++++++++++ src/components/link.tsx | 21 +++ src/components/listbox.tsx | 177 ++++++++++++++++++++++++ src/components/navbar.tsx | 96 +++++++++++++ src/components/pagination.tsx | 101 ++++++++++++++ src/components/radio.tsx | 142 +++++++++++++++++++ src/components/select.tsx | 68 ++++++++++ src/components/sidebar-layout.tsx | 82 +++++++++++ src/components/sidebar.tsx | 142 +++++++++++++++++++ src/components/stacked-layout.tsx | 79 +++++++++++ src/components/switch.tsx | 195 ++++++++++++++++++++++++++ src/components/table.tsx | 124 +++++++++++++++++ src/components/text.tsx | 40 ++++++ src/components/textarea.tsx | 54 ++++++++ 27 files changed, 2685 insertions(+) create mode 100644 src/components/alert.tsx create mode 100644 src/components/auth-layout.tsx create mode 100644 src/components/avatar.tsx create mode 100644 src/components/badge.tsx create mode 100644 src/components/button.tsx create mode 100644 src/components/checkbox.tsx create mode 100644 src/components/combobox.tsx create mode 100644 src/components/description-list.tsx create mode 100644 src/components/dialog.tsx create mode 100644 src/components/divider.tsx create mode 100644 src/components/dropdown.tsx create mode 100644 src/components/fieldset.tsx create mode 100644 src/components/heading.tsx create mode 100644 src/components/input.tsx create mode 100644 src/components/link.tsx create mode 100644 src/components/listbox.tsx create mode 100644 src/components/navbar.tsx create mode 100644 src/components/pagination.tsx create mode 100644 src/components/radio.tsx create mode 100644 src/components/select.tsx create mode 100644 src/components/sidebar-layout.tsx create mode 100644 src/components/sidebar.tsx create mode 100644 src/components/stacked-layout.tsx create mode 100644 src/components/switch.tsx create mode 100644 src/components/table.tsx create mode 100644 src/components/text.tsx create mode 100644 src/components/textarea.tsx diff --git a/src/components/alert.tsx b/src/components/alert.tsx new file mode 100644 index 0000000..9369921 --- /dev/null +++ b/src/components/alert.tsx @@ -0,0 +1,95 @@ +import * as Headless from '@headlessui/react' +import clsx from 'clsx' +import type React from 'react' +import { Text } from './text' + +const sizes = { + xs: 'sm:max-w-xs', + sm: 'sm:max-w-sm', + md: 'sm:max-w-md', + lg: 'sm:max-w-lg', + xl: 'sm:max-w-xl', + '2xl': 'sm:max-w-2xl', + '3xl': 'sm:max-w-3xl', + '4xl': 'sm:max-w-4xl', + '5xl': 'sm:max-w-5xl', +} + +export function Alert({ + size = 'md', + className, + children, + ...props +}: { size?: keyof typeof sizes; className?: string; children: React.ReactNode } & Omit< + Headless.DialogProps, + 'as' | 'className' +>) { + return ( + + + +
+
+ + {children} + +
+
+
+ ) +} + +export function AlertTitle({ + className, + ...props +}: { className?: string } & Omit) { + return ( + + ) +} + +export function AlertDescription({ + className, + ...props +}: { className?: string } & Omit, 'as' | 'className'>) { + return ( + + ) +} + +export function AlertBody({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { + return
+} + +export function AlertActions({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { + return ( +
+ ) +} diff --git a/src/components/auth-layout.tsx b/src/components/auth-layout.tsx new file mode 100644 index 0000000..52309ea --- /dev/null +++ b/src/components/auth-layout.tsx @@ -0,0 +1,11 @@ +import type React from 'react' + +export function AuthLayout({ children }: { children: React.ReactNode }) { + return ( +
+
+ {children} +
+
+ ) +} diff --git a/src/components/avatar.tsx b/src/components/avatar.tsx new file mode 100644 index 0000000..77cfb82 --- /dev/null +++ b/src/components/avatar.tsx @@ -0,0 +1,84 @@ +import * as Headless from '@headlessui/react' +import clsx from 'clsx' +import React, { forwardRef } from 'react' +import { TouchTarget } from './button' +import { Link } from './link' + +type AvatarProps = { + src?: string | null + square?: boolean + initials?: string + alt?: string + className?: string +} + +export function Avatar({ + src = null, + square = false, + initials, + alt = '', + className, + ...props +}: AvatarProps & React.ComponentPropsWithoutRef<'span'>) { + return ( + + {initials && ( + + {alt && {alt}} + + {initials} + + + )} + {src && {alt}} + + ) +} + +export const AvatarButton = forwardRef(function AvatarButton( + { + src, + square = false, + initials, + alt, + className, + ...props + }: AvatarProps & + (Omit | Omit, 'className'>), + ref: React.ForwardedRef +) { + let classes = clsx( + className, + square ? 'rounded-[20%]' : 'rounded-full', + 'relative inline-grid focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500' + ) + + return 'href' in props ? ( + }> + + + + + ) : ( + + + + + + ) +}) diff --git a/src/components/badge.tsx b/src/components/badge.tsx new file mode 100644 index 0000000..8801071 --- /dev/null +++ b/src/components/badge.tsx @@ -0,0 +1,82 @@ +import * as Headless from '@headlessui/react' +import clsx from 'clsx' +import React, { forwardRef } from 'react' +import { TouchTarget } from './button' +import { Link } from './link' + +const colors = { + red: 'bg-red-500/15 text-red-700 group-data-hover:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-hover:bg-red-500/20', + orange: + 'bg-orange-500/15 text-orange-700 group-data-hover:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-hover:bg-orange-500/20', + amber: + 'bg-amber-400/20 text-amber-700 group-data-hover:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-hover:bg-amber-400/15', + yellow: + 'bg-yellow-400/20 text-yellow-700 group-data-hover:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-hover:bg-yellow-400/15', + lime: 'bg-lime-400/20 text-lime-700 group-data-hover:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-hover:bg-lime-400/15', + green: + 'bg-green-500/15 text-green-700 group-data-hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-hover:bg-green-500/20', + emerald: + 'bg-emerald-500/15 text-emerald-700 group-data-hover:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-hover:bg-emerald-500/20', + teal: 'bg-teal-500/15 text-teal-700 group-data-hover:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-hover:bg-teal-500/20', + cyan: 'bg-cyan-400/20 text-cyan-700 group-data-hover:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-hover:bg-cyan-400/15', + sky: 'bg-sky-500/15 text-sky-700 group-data-hover:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-hover:bg-sky-500/20', + blue: 'bg-blue-500/15 text-blue-700 group-data-hover:bg-blue-500/25 dark:text-blue-400 dark:group-data-hover:bg-blue-500/25', + indigo: + 'bg-indigo-500/15 text-indigo-700 group-data-hover:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-hover:bg-indigo-500/20', + violet: + 'bg-violet-500/15 text-violet-700 group-data-hover:bg-violet-500/25 dark:text-violet-400 dark:group-data-hover:bg-violet-500/20', + purple: + 'bg-purple-500/15 text-purple-700 group-data-hover:bg-purple-500/25 dark:text-purple-400 dark:group-data-hover:bg-purple-500/20', + fuchsia: + 'bg-fuchsia-400/15 text-fuchsia-700 group-data-hover:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-hover:bg-fuchsia-400/20', + pink: 'bg-pink-400/15 text-pink-700 group-data-hover:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-hover:bg-pink-400/20', + rose: 'bg-rose-400/15 text-rose-700 group-data-hover:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-hover:bg-rose-400/20', + zinc: 'bg-zinc-600/10 text-zinc-700 group-data-hover:bg-zinc-600/20 dark:bg-white/5 dark:text-zinc-400 dark:group-data-hover:bg-white/10', +} + +type BadgeProps = { color?: keyof typeof colors } + +export function Badge({ color = 'zinc', className, ...props }: BadgeProps & React.ComponentPropsWithoutRef<'span'>) { + return ( + + ) +} + +export const BadgeButton = forwardRef(function BadgeButton( + { + color = 'zinc', + className, + children, + ...props + }: BadgeProps & { className?: string; children: React.ReactNode } & ( + | Omit + | Omit, 'className'> + ), + ref: React.ForwardedRef +) { + let classes = clsx( + className, + 'group relative inline-flex rounded-md focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500' + ) + + return 'href' in props ? ( + }> + + {children} + + + ) : ( + + + {children} + + + ) +}) diff --git a/src/components/button.tsx b/src/components/button.tsx new file mode 100644 index 0000000..5b685df --- /dev/null +++ b/src/components/button.tsx @@ -0,0 +1,204 @@ +import * as Headless from '@headlessui/react' +import clsx from 'clsx' +import React, { forwardRef } from 'react' +import { Link } from './link' + +const styles = { + base: [ + // Base + 'relative isolate inline-flex items-baseline justify-center gap-x-2 rounded-lg border text-base/6 font-semibold', + // Sizing + 'px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)] sm:text-sm/6', + // Focus + 'focus:outline-hidden data-focus:outline data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500', + // Disabled + 'data-disabled:opacity-50', + // Icon + '*:data-[slot=icon]:-mx-0.5 *:data-[slot=icon]:my-0.5 *:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:self-center *:data-[slot=icon]:text-(--btn-icon) sm:*:data-[slot=icon]:my-1 sm:*:data-[slot=icon]:size-4 forced-colors:[--btn-icon:ButtonText] forced-colors:data-hover:[--btn-icon:ButtonText]', + ], + solid: [ + // Optical border, implemented as the button background to avoid corner artifacts + 'border-transparent bg-(--btn-border)', + // Dark mode: border is rendered on `after` so background is set to button background + 'dark:bg-(--btn-bg)', + // Button background, implemented as foreground layer to stack on top of pseudo-border layer + 'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-lg)-1px)] before:bg-(--btn-bg)', + // Drop shadow, applied to the inset `before` layer so it blends with the border + 'before:shadow-sm', + // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo + 'dark:before:hidden', + // Dark mode: Subtle white outline is applied using a border + 'dark:border-white/5', + // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow + 'after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius-lg)-1px)]', + // Inner highlight shadow + 'after:shadow-[shadow:inset_0_1px_--theme(--color-white/15%)]', + // White overlay on hover + 'data-active:after:bg-(--btn-hover-overlay) data-hover:after:bg-(--btn-hover-overlay)', + // Dark mode: `after` layer expands to cover entire button + 'dark:after:-inset-px dark:after:rounded-lg', + // Disabled + 'data-disabled:before:shadow-none data-disabled:after:shadow-none', + ], + outline: [ + // Base + 'border-zinc-950/10 text-zinc-950 data-active:bg-zinc-950/[2.5%] data-hover:bg-zinc-950/[2.5%]', + // Dark mode + 'dark:border-white/15 dark:text-white dark:[--btn-bg:transparent] dark:data-active:bg-white/5 dark:data-hover:bg-white/5', + // Icon + '[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]', + ], + plain: [ + // Base + 'border-transparent text-zinc-950 data-active:bg-zinc-950/5 data-hover:bg-zinc-950/5', + // Dark mode + 'dark:text-white dark:data-active:bg-white/10 dark:data-hover:bg-white/10', + // Icon + '[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]', + ], + colors: { + 'dark/zinc': [ + 'text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10', + 'dark:text-white dark:[--btn-bg:var(--color-zinc-600)] dark:[--btn-hover-overlay:var(--color-white)]/5', + '[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]', + ], + light: [ + 'text-zinc-950 [--btn-bg:white] [--btn-border:var(--color-zinc-950)]/10 [--btn-hover-overlay:var(--color-zinc-950)]/[2.5%] data-active:[--btn-border:var(--color-zinc-950)]/15 data-hover:[--btn-border:var(--color-zinc-950)]/15', + 'dark:text-white dark:[--btn-hover-overlay:var(--color-white)]/5 dark:[--btn-bg:var(--color-zinc-800)]', + '[--btn-icon:var(--color-zinc-500)] data-active:[--btn-icon:var(--color-zinc-700)] data-hover:[--btn-icon:var(--color-zinc-700)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]', + ], + 'dark/white': [ + 'text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10', + 'dark:text-zinc-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:var(--color-zinc-950)]/5', + '[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)] dark:[--btn-icon:var(--color-zinc-500)] dark:data-active:[--btn-icon:var(--color-zinc-400)] dark:data-hover:[--btn-icon:var(--color-zinc-400)]', + ], + dark: [ + 'text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10', + 'dark:[--btn-hover-overlay:var(--color-white)]/5 dark:[--btn-bg:var(--color-zinc-800)]', + '[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]', + ], + white: [ + 'text-zinc-950 [--btn-bg:white] [--btn-border:var(--color-zinc-950)]/10 [--btn-hover-overlay:var(--color-zinc-950)]/[2.5%] data-active:[--btn-border:var(--color-zinc-950)]/15 data-hover:[--btn-border:var(--color-zinc-950)]/15', + 'dark:[--btn-hover-overlay:var(--color-zinc-950)]/5', + '[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-500)] data-hover:[--btn-icon:var(--color-zinc-500)]', + ], + zinc: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-zinc-600)] [--btn-border:var(--color-zinc-700)]/90', + 'dark:[--btn-hover-overlay:var(--color-white)]/5', + '[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]', + ], + indigo: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-indigo-500)] [--btn-border:var(--color-indigo-600)]/90', + '[--btn-icon:var(--color-indigo-300)] data-active:[--btn-icon:var(--color-indigo-200)] data-hover:[--btn-icon:var(--color-indigo-200)]', + ], + cyan: [ + 'text-cyan-950 [--btn-bg:var(--color-cyan-300)] [--btn-border:var(--color-cyan-400)]/80 [--btn-hover-overlay:var(--color-white)]/25', + '[--btn-icon:var(--color-cyan-500)]', + ], + red: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-red-600)] [--btn-border:var(--color-red-700)]/90', + '[--btn-icon:var(--color-red-300)] data-active:[--btn-icon:var(--color-red-200)] data-hover:[--btn-icon:var(--color-red-200)]', + ], + orange: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-orange-500)] [--btn-border:var(--color-orange-600)]/90', + '[--btn-icon:var(--color-orange-300)] data-active:[--btn-icon:var(--color-orange-200)] data-hover:[--btn-icon:var(--color-orange-200)]', + ], + amber: [ + 'text-amber-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-amber-400)] [--btn-border:var(--color-amber-500)]/80', + '[--btn-icon:var(--color-amber-600)]', + ], + yellow: [ + 'text-yellow-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-yellow-300)] [--btn-border:var(--color-yellow-400)]/80', + '[--btn-icon:var(--color-yellow-600)] data-active:[--btn-icon:var(--color-yellow-700)] data-hover:[--btn-icon:var(--color-yellow-700)]', + ], + lime: [ + 'text-lime-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-lime-300)] [--btn-border:var(--color-lime-400)]/80', + '[--btn-icon:var(--color-lime-600)] data-active:[--btn-icon:var(--color-lime-700)] data-hover:[--btn-icon:var(--color-lime-700)]', + ], + green: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-green-600)] [--btn-border:var(--color-green-700)]/90', + '[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80', + ], + emerald: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-emerald-600)] [--btn-border:var(--color-emerald-700)]/90', + '[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80', + ], + teal: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-teal-600)] [--btn-border:var(--color-teal-700)]/90', + '[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80', + ], + sky: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-sky-500)] [--btn-border:var(--color-sky-600)]/80', + '[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80', + ], + blue: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-blue-600)] [--btn-border:var(--color-blue-700)]/90', + '[--btn-icon:var(--color-blue-400)] data-active:[--btn-icon:var(--color-blue-300)] data-hover:[--btn-icon:var(--color-blue-300)]', + ], + violet: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-violet-500)] [--btn-border:var(--color-violet-600)]/90', + '[--btn-icon:var(--color-violet-300)] data-active:[--btn-icon:var(--color-violet-200)] data-hover:[--btn-icon:var(--color-violet-200)]', + ], + purple: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-purple-500)] [--btn-border:var(--color-purple-600)]/90', + '[--btn-icon:var(--color-purple-300)] data-active:[--btn-icon:var(--color-purple-200)] data-hover:[--btn-icon:var(--color-purple-200)]', + ], + fuchsia: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-fuchsia-500)] [--btn-border:var(--color-fuchsia-600)]/90', + '[--btn-icon:var(--color-fuchsia-300)] data-active:[--btn-icon:var(--color-fuchsia-200)] data-hover:[--btn-icon:var(--color-fuchsia-200)]', + ], + pink: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-pink-500)] [--btn-border:var(--color-pink-600)]/90', + '[--btn-icon:var(--color-pink-300)] data-active:[--btn-icon:var(--color-pink-200)] data-hover:[--btn-icon:var(--color-pink-200)]', + ], + rose: [ + 'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-rose-500)] [--btn-border:var(--color-rose-600)]/90', + '[--btn-icon:var(--color-rose-300)] data-active:[--btn-icon:var(--color-rose-200)] data-hover:[--btn-icon:var(--color-rose-200)]', + ], + }, +} + +type ButtonProps = ( + | { color?: keyof typeof styles.colors; outline?: never; plain?: never } + | { color?: never; outline: true; plain?: never } + | { color?: never; outline?: never; plain: true } +) & { className?: string; children: React.ReactNode } & ( + | Omit + | Omit, 'className'> + ) + +export const Button = forwardRef(function Button( + { color, outline, plain, className, children, ...props }: ButtonProps, + ref: React.ForwardedRef +) { + let classes = clsx( + className, + styles.base, + outline ? styles.outline : plain ? styles.plain : clsx(styles.solid, styles.colors[color ?? 'dark/zinc']) + ) + + return 'href' in props ? ( + }> + {children} + + ) : ( + + {children} + + ) +}) + +/** + * Expand the hit area to at least 44×44px on touch devices + */ +export function TouchTarget({ children }: { children: React.ReactNode }) { + return ( + <> +