diff --git a/index.html b/index.html index e4b78ea..5d3da5f 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ Vite + React + TS - +
diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx index afe48ac..b1578bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,34 +1,10 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import React from 'react' +import { Layout } from './components/layout' -function App() { - const [count, setCount] = useState(0) +function App({children}: {children: React.ReactNode}) { return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- + {children} ) } diff --git a/src/components/layout.tsx b/src/components/layout.tsx new file mode 100644 index 0000000..49f7e7e --- /dev/null +++ b/src/components/layout.tsx @@ -0,0 +1,193 @@ +import { Avatar } from './ui/avatar' +import { + Dropdown, + DropdownButton, + DropdownDivider, + DropdownItem, + DropdownLabel, + DropdownMenu, +} from './ui/dropdown' +import { Navbar, NavbarItem, NavbarSection, NavbarSpacer } from './ui/navbar' +import { + Sidebar, + SidebarBody, + SidebarFooter, + SidebarHeader, + SidebarHeading, + SidebarItem, + SidebarLabel, + SidebarSection, + SidebarSpacer, +} from './ui/sidebar' +import { SidebarLayout } from './ui/sidebar-layout' +import { + ArrowRightStartOnRectangleIcon, + ChevronDownIcon, + ChevronUpIcon, + Cog8ToothIcon, + LightBulbIcon, + PlusIcon, + ShieldCheckIcon, + UserCircleIcon, +} from '@heroicons/react/16/solid' +import { + Cog6ToothIcon, + HomeIcon, + QuestionMarkCircleIcon, + SparklesIcon, + Square2StackIcon, + TicketIcon, +} from '@heroicons/react/20/solid' + +function AccountDropdownMenu({ anchor }: { anchor: 'top start' | 'bottom end' }) { + return ( + + + + My account + + + + + Privacy policy + + + + Share feedback + + + + + Sign out + + + ) +} + +const events = [ + { id: 1, name: 'Catalyst Launch Party', url: '/events/1' }, + { id: 2, name: 'Big Events Conference', url: '/events/2' }, + { id: 3, name: 'Big Events Expo', url: '/events/3' }, + { id: 4, name: 'Big Events Summit', url: '/events/4' }, +] + +export function Layout({ + children, +}: { + children: React.ReactNode +}) { + + return ( + + + + + + + + + + + + } + sidebar={ + + + + + + Catalyst + + + + + + Settings + + + + + Catalyst + + + + Big Events + + + + + New team… + + + + + + + + + + Home + + + + Events + + + + Orders + + + + Settings + + + + + Upcoming Events + {events.map((event) => ( + + {event.name} + + ))} + + + + + + + + Support + + + + Changelog + + + + + + + + + + + Erica + + erica@example.com + + + + + + + + + + } + > + {children} + + ) +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..6702031 --- /dev/null +++ b/src/components/ui/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, + 'className' +>) { + return ( + + + +
+
+ + {children} + +
+
+
+ ) +} + +export function AlertTitle({ + className, + ...props +}: { className?: string } & Omit) { + return ( + + ) +} + +export function AlertDescription({ + className, + ...props +}: { className?: string } & Omit, '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/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..ed21e31 --- /dev/null +++ b/src/components/ui/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-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500' + ) + + return 'href' in props ? ( + }> + + + + + ) : ( + + + + + + ) +}) diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..20a919a --- /dev/null +++ b/src/components/ui/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-none data-[focus]:outline 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/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..2153981 --- /dev/null +++ b/src/components/ui/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-center justify-center gap-x-2 rounded-lg border text-base/6 font-semibold', + // Sizing + 'px-[calc(theme(spacing[3.5])-1px)] py-[calc(theme(spacing[2.5])-1px)] sm:px-[calc(theme(spacing.3)-1px)] sm:py-[calc(theme(spacing[1.5])-1px)] sm:text-sm/6', + // Focus + 'focus:outline-none 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]]:text-[--btn-icon] [&>[data-slot=icon]]:sm:my-1 [&>[data-slot=icon]]:sm: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(theme(borderRadius.lg)-1px)] before:bg-[--btn-bg]', + // Drop shadow, applied to the inset `before` layer so it blends with the border + 'before:shadow', + // 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(theme(borderRadius.lg)-1px)]', + // Inner highlight shadow + 'after:shadow-[shadow:inset_0_1px_theme(colors.white/15%)]', + // White overlay on hover + 'after:data-[active]:bg-[--btn-hover-overlay] after:data-[hover]:bg-[--btn-hover-overlay]', + // Dark mode: `after` layer expands to cover entire button + 'dark:after:-inset-px dark:after:rounded-lg', + // Disabled + 'before:data-[disabled]:shadow-none after:data-[disabled]: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:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.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:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]', + ], + colors: { + 'dark/zinc': [ + 'text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]', + 'dark:text-white dark:[--btn-bg:theme(colors.zinc.600)] dark:[--btn-hover-overlay:theme(colors.white/5%)]', + '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]', + ], + light: [ + 'text-zinc-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]', + 'dark:text-white dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]', + '[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]', + ], + 'dark/white': [ + 'text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]', + 'dark:text-zinc-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]', + '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]', + ], + dark: [ + 'text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]', + 'dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]', + '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]', + ], + white: [ + 'text-zinc-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]', + 'dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]', + '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.500)] data-[hover]:[--btn-icon:theme(colors.zinc.500)]', + ], + zinc: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.zinc.600)] [--btn-border:theme(colors.zinc.700/90%)]', + 'dark:[--btn-hover-overlay:theme(colors.white/5%)]', + '[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]', + ], + indigo: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.indigo.500)] [--btn-border:theme(colors.indigo.600/90%)]', + '[--btn-icon:theme(colors.indigo.300)] data-[active]:[--btn-icon:theme(colors.indigo.200)] data-[hover]:[--btn-icon:theme(colors.indigo.200)]', + ], + cyan: [ + 'text-cyan-950 [--btn-bg:theme(colors.cyan.300)] [--btn-border:theme(colors.cyan.400/80%)] [--btn-hover-overlay:theme(colors.white/25%)]', + '[--btn-icon:theme(colors.cyan.500)]', + ], + red: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.red.600)] [--btn-border:theme(colors.red.700/90%)]', + '[--btn-icon:theme(colors.red.300)] data-[active]:[--btn-icon:theme(colors.red.200)] data-[hover]:[--btn-icon:theme(colors.red.200)]', + ], + orange: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.orange.500)] [--btn-border:theme(colors.orange.600/90%)]', + '[--btn-icon:theme(colors.orange.300)] data-[active]:[--btn-icon:theme(colors.orange.200)] data-[hover]:[--btn-icon:theme(colors.orange.200)]', + ], + amber: [ + 'text-amber-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.amber.400)] [--btn-border:theme(colors.amber.500/80%)]', + '[--btn-icon:theme(colors.amber.600)]', + ], + yellow: [ + 'text-yellow-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.yellow.300)] [--btn-border:theme(colors.yellow.400/80%)]', + '[--btn-icon:theme(colors.yellow.600)] data-[active]:[--btn-icon:theme(colors.yellow.700)] data-[hover]:[--btn-icon:theme(colors.yellow.700)]', + ], + lime: [ + 'text-lime-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.lime.300)] [--btn-border:theme(colors.lime.400/80%)]', + '[--btn-icon:theme(colors.lime.600)] data-[active]:[--btn-icon:theme(colors.lime.700)] data-[hover]:[--btn-icon:theme(colors.lime.700)]', + ], + green: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.green.600)] [--btn-border:theme(colors.green.700/90%)]', + '[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]', + ], + emerald: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.emerald.600)] [--btn-border:theme(colors.emerald.700/90%)]', + '[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]', + ], + teal: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.teal.600)] [--btn-border:theme(colors.teal.700/90%)]', + '[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]', + ], + sky: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.sky.500)] [--btn-border:theme(colors.sky.600/80%)]', + '[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]', + ], + blue: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.blue.600)] [--btn-border:theme(colors.blue.700/90%)]', + '[--btn-icon:theme(colors.blue.400)] data-[active]:[--btn-icon:theme(colors.blue.300)] data-[hover]:[--btn-icon:theme(colors.blue.300)]', + ], + violet: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.violet.500)] [--btn-border:theme(colors.violet.600/90%)]', + '[--btn-icon:theme(colors.violet.300)] data-[active]:[--btn-icon:theme(colors.violet.200)] data-[hover]:[--btn-icon:theme(colors.violet.200)]', + ], + purple: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.purple.500)] [--btn-border:theme(colors.purple.600/90%)]', + '[--btn-icon:theme(colors.purple.300)] data-[active]:[--btn-icon:theme(colors.purple.200)] data-[hover]:[--btn-icon:theme(colors.purple.200)]', + ], + fuchsia: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.fuchsia.500)] [--btn-border:theme(colors.fuchsia.600/90%)]', + '[--btn-icon:theme(colors.fuchsia.300)] data-[active]:[--btn-icon:theme(colors.fuchsia.200)] data-[hover]:[--btn-icon:theme(colors.fuchsia.200)]', + ], + pink: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.pink.500)] [--btn-border:theme(colors.pink.600/90%)]', + '[--btn-icon:theme(colors.pink.300)] data-[active]:[--btn-icon:theme(colors.pink.200)] data-[hover]:[--btn-icon:theme(colors.pink.200)]', + ], + rose: [ + 'text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.rose.500)] [--btn-border:theme(colors.rose.600/90%)]', + '[--btn-icon:theme(colors.rose.300)] data-[active]:[--btn-icon:theme(colors.rose.200)] data-[hover]:[--btn-icon:theme(colors.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 ( + <> +