@taicode/common-web 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -5
- package/source/catalyst/alert.tsx +95 -0
- package/source/catalyst/auth-layout.tsx +11 -0
- package/source/catalyst/avatar.tsx +84 -0
- package/source/catalyst/badge.tsx +82 -0
- package/source/catalyst/button.tsx +204 -0
- package/source/catalyst/checkbox.tsx +157 -0
- package/source/catalyst/combobox.tsx +188 -0
- package/source/catalyst/description-list.tsx +37 -0
- package/source/catalyst/dialog.tsx +86 -0
- package/source/catalyst/divider.tsx +20 -0
- package/source/catalyst/dropdown.tsx +188 -0
- package/source/catalyst/fieldset.tsx +91 -0
- package/source/catalyst/heading.tsx +27 -0
- package/source/catalyst/input.tsx +94 -0
- package/source/catalyst/link.tsx +21 -0
- package/source/catalyst/listbox.tsx +177 -0
- package/source/catalyst/navbar.tsx +96 -0
- package/source/catalyst/pagination.tsx +98 -0
- package/source/catalyst/radio.tsx +142 -0
- package/source/catalyst/select.tsx +68 -0
- package/source/catalyst/sidebar-layout.tsx +82 -0
- package/source/catalyst/sidebar.tsx +142 -0
- package/source/catalyst/stacked-layout.tsx +79 -0
- package/source/catalyst/switch.tsx +195 -0
- package/source/catalyst/table.tsx +124 -0
- package/source/catalyst/text.tsx +40 -0
- package/source/catalyst/textarea.tsx +54 -0
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taicode/common-web",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"author": "Alain",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"description": "",
|
|
@@ -9,12 +9,24 @@
|
|
|
9
9
|
"dev": "tsc -p tsconfig.json --watch"
|
|
10
10
|
},
|
|
11
11
|
"peerDependencies": {
|
|
12
|
+
"@types/react": ">=18",
|
|
12
13
|
"mobx": ">=6",
|
|
13
|
-
"react": ">=18"
|
|
14
|
-
"@types/react": ">=18"
|
|
14
|
+
"react": ">=18"
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
|
-
"./utils/*": [
|
|
18
|
-
|
|
17
|
+
"./utils/*": [
|
|
18
|
+
"./output/utils/*"
|
|
19
|
+
],
|
|
20
|
+
"./hooks/*": [
|
|
21
|
+
"./output/hooks/*"
|
|
22
|
+
],
|
|
23
|
+
"./catalyst/*": [
|
|
24
|
+
"./output/catalyst/*"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@headlessui/react": "^2.2.4",
|
|
29
|
+
"clsx": "^2.1.1",
|
|
30
|
+
"framer-motion": "^12.19.2"
|
|
19
31
|
}
|
|
20
32
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as Headless from '@headlessui/react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import type React from 'react'
|
|
4
|
+
import { Text } from './text'
|
|
5
|
+
|
|
6
|
+
const sizes = {
|
|
7
|
+
xs: 'sm:max-w-xs',
|
|
8
|
+
sm: 'sm:max-w-sm',
|
|
9
|
+
md: 'sm:max-w-md',
|
|
10
|
+
lg: 'sm:max-w-lg',
|
|
11
|
+
xl: 'sm:max-w-xl',
|
|
12
|
+
'2xl': 'sm:max-w-2xl',
|
|
13
|
+
'3xl': 'sm:max-w-3xl',
|
|
14
|
+
'4xl': 'sm:max-w-4xl',
|
|
15
|
+
'5xl': 'sm:max-w-5xl',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Alert({
|
|
19
|
+
size = 'md',
|
|
20
|
+
className,
|
|
21
|
+
children,
|
|
22
|
+
...props
|
|
23
|
+
}: { size?: keyof typeof sizes; className?: string; children: React.ReactNode } & Omit<
|
|
24
|
+
Headless.DialogProps,
|
|
25
|
+
'as' | 'className'
|
|
26
|
+
>) {
|
|
27
|
+
return (
|
|
28
|
+
<Headless.Dialog {...props}>
|
|
29
|
+
<Headless.DialogBackdrop
|
|
30
|
+
transition
|
|
31
|
+
className="fixed inset-0 flex w-screen justify-center overflow-y-auto bg-zinc-950/15 px-2 py-2 transition duration-100 focus:outline-0 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16 dark:bg-zinc-950/50"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<div className="fixed inset-0 w-screen overflow-y-auto pt-6 sm:pt-0">
|
|
35
|
+
<div className="grid min-h-full grid-rows-[1fr_auto_1fr] justify-items-center p-8 sm:grid-rows-[1fr_auto_3fr] sm:p-4">
|
|
36
|
+
<Headless.DialogPanel
|
|
37
|
+
transition
|
|
38
|
+
className={clsx(
|
|
39
|
+
className,
|
|
40
|
+
sizes[size],
|
|
41
|
+
'row-start-2 w-full rounded-2xl bg-white p-8 shadow-lg ring-1 ring-zinc-950/10 sm:rounded-2xl sm:p-6 dark:bg-zinc-900 dark:ring-white/10 forced-colors:outline',
|
|
42
|
+
'transition duration-100 will-change-transform data-closed:opacity-0 data-enter:ease-out data-closed:data-enter:scale-95 data-leave:ease-in'
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</Headless.DialogPanel>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</Headless.Dialog>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function AlertTitle({
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
}: { className?: string } & Omit<Headless.DialogTitleProps, 'as' | 'className'>) {
|
|
57
|
+
return (
|
|
58
|
+
<Headless.DialogTitle
|
|
59
|
+
{...props}
|
|
60
|
+
className={clsx(
|
|
61
|
+
className,
|
|
62
|
+
'text-center text-base/6 font-semibold text-balance text-zinc-950 sm:text-left sm:text-sm/6 sm:text-wrap dark:text-white'
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function AlertDescription({
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: { className?: string } & Omit<Headless.DescriptionProps<typeof Text>, 'as' | 'className'>) {
|
|
72
|
+
return (
|
|
73
|
+
<Headless.Description
|
|
74
|
+
as={Text}
|
|
75
|
+
{...props}
|
|
76
|
+
className={clsx(className, 'mt-2 text-center text-pretty sm:text-left')}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function AlertBody({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
|
82
|
+
return <div {...props} className={clsx(className, 'mt-4')} />
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function AlertActions({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
{...props}
|
|
89
|
+
className={clsx(
|
|
90
|
+
className,
|
|
91
|
+
'mt-6 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:mt-4 sm:flex-row sm:*:w-auto'
|
|
92
|
+
)}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
3
|
+
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<main className="flex min-h-dvh flex-col p-2">
|
|
6
|
+
<div className="flex grow items-center justify-center p-6 lg:rounded-lg lg:bg-white lg:p-10 lg:shadow-xs lg:ring-1 lg:ring-zinc-950/5 dark:lg:bg-zinc-900 dark:lg:ring-white/10">
|
|
7
|
+
{children}
|
|
8
|
+
</div>
|
|
9
|
+
</main>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as Headless from '@headlessui/react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import React, { forwardRef } from 'react'
|
|
4
|
+
import { TouchTarget } from './button'
|
|
5
|
+
import { Link } from './link'
|
|
6
|
+
|
|
7
|
+
type AvatarProps = {
|
|
8
|
+
src?: string | null
|
|
9
|
+
square?: boolean
|
|
10
|
+
initials?: string
|
|
11
|
+
alt?: string
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Avatar({
|
|
16
|
+
src = null,
|
|
17
|
+
square = false,
|
|
18
|
+
initials,
|
|
19
|
+
alt = '',
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: AvatarProps & React.ComponentPropsWithoutRef<'span'>) {
|
|
23
|
+
return (
|
|
24
|
+
<span
|
|
25
|
+
data-slot="avatar"
|
|
26
|
+
{...props}
|
|
27
|
+
className={clsx(
|
|
28
|
+
className,
|
|
29
|
+
// Basic layout
|
|
30
|
+
'inline-grid shrink-0 align-middle [--avatar-radius:20%] *:col-start-1 *:row-start-1',
|
|
31
|
+
'outline -outline-offset-1 outline-black/10 dark:outline-white/10',
|
|
32
|
+
// Border radius
|
|
33
|
+
square ? 'rounded-(--avatar-radius) *:rounded-(--avatar-radius)' : 'rounded-full *:rounded-full'
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{initials && (
|
|
37
|
+
<svg
|
|
38
|
+
className="size-full fill-current p-[5%] text-[48px] font-medium uppercase select-none"
|
|
39
|
+
viewBox="0 0 100 100"
|
|
40
|
+
aria-hidden={alt ? undefined : 'true'}
|
|
41
|
+
>
|
|
42
|
+
{alt && <title>{alt}</title>}
|
|
43
|
+
<text x="50%" y="50%" alignmentBaseline="middle" dominantBaseline="middle" textAnchor="middle" dy=".125em">
|
|
44
|
+
{initials}
|
|
45
|
+
</text>
|
|
46
|
+
</svg>
|
|
47
|
+
)}
|
|
48
|
+
{src && <img className="size-full" src={src} alt={alt} />}
|
|
49
|
+
</span>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const AvatarButton = forwardRef(function AvatarButton(
|
|
54
|
+
{
|
|
55
|
+
src,
|
|
56
|
+
square = false,
|
|
57
|
+
initials,
|
|
58
|
+
alt,
|
|
59
|
+
className,
|
|
60
|
+
...props
|
|
61
|
+
}: AvatarProps &
|
|
62
|
+
(Omit<Headless.ButtonProps, 'as' | 'className'> | Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>),
|
|
63
|
+
ref: React.ForwardedRef<HTMLElement>
|
|
64
|
+
) {
|
|
65
|
+
let classes = clsx(
|
|
66
|
+
className,
|
|
67
|
+
square ? 'rounded-[20%]' : 'rounded-full',
|
|
68
|
+
'relative inline-grid focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return 'href' in props ? (
|
|
72
|
+
<Link {...props} className={classes} ref={ref as React.ForwardedRef<HTMLAnchorElement>}>
|
|
73
|
+
<TouchTarget>
|
|
74
|
+
<Avatar src={src} square={square} initials={initials} alt={alt} />
|
|
75
|
+
</TouchTarget>
|
|
76
|
+
</Link>
|
|
77
|
+
) : (
|
|
78
|
+
<Headless.Button {...props} className={classes} ref={ref}>
|
|
79
|
+
<TouchTarget>
|
|
80
|
+
<Avatar src={src} square={square} initials={initials} alt={alt} />
|
|
81
|
+
</TouchTarget>
|
|
82
|
+
</Headless.Button>
|
|
83
|
+
)
|
|
84
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as Headless from '@headlessui/react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import React, { forwardRef } from 'react'
|
|
4
|
+
import { TouchTarget } from './button'
|
|
5
|
+
import { Link } from './link'
|
|
6
|
+
|
|
7
|
+
const colors = {
|
|
8
|
+
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',
|
|
9
|
+
orange:
|
|
10
|
+
'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',
|
|
11
|
+
amber:
|
|
12
|
+
'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',
|
|
13
|
+
yellow:
|
|
14
|
+
'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',
|
|
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',
|
|
16
|
+
green:
|
|
17
|
+
'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',
|
|
18
|
+
emerald:
|
|
19
|
+
'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',
|
|
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',
|
|
21
|
+
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',
|
|
22
|
+
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',
|
|
23
|
+
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',
|
|
24
|
+
indigo:
|
|
25
|
+
'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',
|
|
26
|
+
violet:
|
|
27
|
+
'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',
|
|
28
|
+
purple:
|
|
29
|
+
'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',
|
|
30
|
+
fuchsia:
|
|
31
|
+
'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',
|
|
32
|
+
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',
|
|
33
|
+
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',
|
|
34
|
+
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',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type BadgeProps = { color?: keyof typeof colors }
|
|
38
|
+
|
|
39
|
+
export function Badge({ color = 'zinc', className, ...props }: BadgeProps & React.ComponentPropsWithoutRef<'span'>) {
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
{...props}
|
|
43
|
+
className={clsx(
|
|
44
|
+
className,
|
|
45
|
+
'inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-sm/5 font-medium sm:text-xs/5 forced-colors:outline',
|
|
46
|
+
colors[color]
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const BadgeButton = forwardRef(function BadgeButton(
|
|
53
|
+
{
|
|
54
|
+
color = 'zinc',
|
|
55
|
+
className,
|
|
56
|
+
children,
|
|
57
|
+
...props
|
|
58
|
+
}: BadgeProps & { className?: string; children: React.ReactNode } & (
|
|
59
|
+
| Omit<Headless.ButtonProps, 'as' | 'className'>
|
|
60
|
+
| Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>
|
|
61
|
+
),
|
|
62
|
+
ref: React.ForwardedRef<HTMLElement>
|
|
63
|
+
) {
|
|
64
|
+
let classes = clsx(
|
|
65
|
+
className,
|
|
66
|
+
'group relative inline-flex rounded-md focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return 'href' in props ? (
|
|
70
|
+
<Link {...props} className={classes} ref={ref as React.ForwardedRef<HTMLAnchorElement>}>
|
|
71
|
+
<TouchTarget>
|
|
72
|
+
<Badge color={color}>{children}</Badge>
|
|
73
|
+
</TouchTarget>
|
|
74
|
+
</Link>
|
|
75
|
+
) : (
|
|
76
|
+
<Headless.Button {...props} className={classes} ref={ref}>
|
|
77
|
+
<TouchTarget>
|
|
78
|
+
<Badge color={color}>{children}</Badge>
|
|
79
|
+
</TouchTarget>
|
|
80
|
+
</Headless.Button>
|
|
81
|
+
)
|
|
82
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as Headless from '@headlessui/react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import React, { forwardRef } from 'react'
|
|
4
|
+
import { Link } from './link'
|
|
5
|
+
|
|
6
|
+
const styles = {
|
|
7
|
+
base: [
|
|
8
|
+
// Base
|
|
9
|
+
'relative isolate inline-flex items-baseline justify-center gap-x-2 rounded-lg border text-base/6 font-semibold',
|
|
10
|
+
// Sizing
|
|
11
|
+
'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',
|
|
12
|
+
// Focus
|
|
13
|
+
'focus:not-data-focus:outline-hidden data-focus:outline-2 data-focus:outline-offset-2 data-focus:outline-blue-500',
|
|
14
|
+
// Disabled
|
|
15
|
+
'data-disabled:opacity-50',
|
|
16
|
+
// Icon
|
|
17
|
+
'*: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]',
|
|
18
|
+
],
|
|
19
|
+
solid: [
|
|
20
|
+
// Optical border, implemented as the button background to avoid corner artifacts
|
|
21
|
+
'border-transparent bg-(--btn-border)',
|
|
22
|
+
// Dark mode: border is rendered on `after` so background is set to button background
|
|
23
|
+
'dark:bg-(--btn-bg)',
|
|
24
|
+
// Button background, implemented as foreground layer to stack on top of pseudo-border layer
|
|
25
|
+
'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(var(--radius-lg)-1px)] before:bg-(--btn-bg)',
|
|
26
|
+
// Drop shadow, applied to the inset `before` layer so it blends with the border
|
|
27
|
+
'before:shadow-sm',
|
|
28
|
+
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
|
|
29
|
+
'dark:before:hidden',
|
|
30
|
+
// Dark mode: Subtle white outline is applied using a border
|
|
31
|
+
'dark:border-white/5',
|
|
32
|
+
// Shim/overlay, inset to match button foreground and used for hover state + highlight shadow
|
|
33
|
+
'after:absolute after:inset-0 after:-z-10 after:rounded-[calc(var(--radius-lg)-1px)]',
|
|
34
|
+
// Inner highlight shadow
|
|
35
|
+
'after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
|
|
36
|
+
// White overlay on hover
|
|
37
|
+
'data-active:after:bg-(--btn-hover-overlay) data-hover:after:bg-(--btn-hover-overlay)',
|
|
38
|
+
// Dark mode: `after` layer expands to cover entire button
|
|
39
|
+
'dark:after:-inset-px dark:after:rounded-lg',
|
|
40
|
+
// Disabled
|
|
41
|
+
'data-disabled:before:shadow-none data-disabled:after:shadow-none',
|
|
42
|
+
],
|
|
43
|
+
outline: [
|
|
44
|
+
// Base
|
|
45
|
+
'border-zinc-950/10 text-zinc-950 data-active:bg-zinc-950/2.5 data-hover:bg-zinc-950/2.5',
|
|
46
|
+
// Dark mode
|
|
47
|
+
'dark:border-white/15 dark:text-white dark:[--btn-bg:transparent] dark:data-active:bg-white/5 dark:data-hover:bg-white/5',
|
|
48
|
+
// Icon
|
|
49
|
+
'[--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)]',
|
|
50
|
+
],
|
|
51
|
+
plain: [
|
|
52
|
+
// Base
|
|
53
|
+
'border-transparent text-zinc-950 data-active:bg-zinc-950/5 data-hover:bg-zinc-950/5',
|
|
54
|
+
// Dark mode
|
|
55
|
+
'dark:text-white dark:data-active:bg-white/10 dark:data-hover:bg-white/10',
|
|
56
|
+
// Icon
|
|
57
|
+
'[--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)]',
|
|
58
|
+
],
|
|
59
|
+
colors: {
|
|
60
|
+
'dark/zinc': [
|
|
61
|
+
'text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10',
|
|
62
|
+
'dark:text-white dark:[--btn-bg:var(--color-zinc-600)] dark:[--btn-hover-overlay:var(--color-white)]/5',
|
|
63
|
+
'[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]',
|
|
64
|
+
],
|
|
65
|
+
light: [
|
|
66
|
+
'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',
|
|
67
|
+
'dark:text-white dark:[--btn-hover-overlay:var(--color-white)]/5 dark:[--btn-bg:var(--color-zinc-800)]',
|
|
68
|
+
'[--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)]',
|
|
69
|
+
],
|
|
70
|
+
'dark/white': [
|
|
71
|
+
'text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10',
|
|
72
|
+
'dark:text-zinc-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:var(--color-zinc-950)]/5',
|
|
73
|
+
'[--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)]',
|
|
74
|
+
],
|
|
75
|
+
dark: [
|
|
76
|
+
'text-white [--btn-bg:var(--color-zinc-900)] [--btn-border:var(--color-zinc-950)]/90 [--btn-hover-overlay:var(--color-white)]/10',
|
|
77
|
+
'dark:[--btn-hover-overlay:var(--color-white)]/5 dark:[--btn-bg:var(--color-zinc-800)]',
|
|
78
|
+
'[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]',
|
|
79
|
+
],
|
|
80
|
+
white: [
|
|
81
|
+
'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',
|
|
82
|
+
'dark:[--btn-hover-overlay:var(--color-zinc-950)]/5',
|
|
83
|
+
'[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-500)] data-hover:[--btn-icon:var(--color-zinc-500)]',
|
|
84
|
+
],
|
|
85
|
+
zinc: [
|
|
86
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-zinc-600)] [--btn-border:var(--color-zinc-700)]/90',
|
|
87
|
+
'dark:[--btn-hover-overlay:var(--color-white)]/5',
|
|
88
|
+
'[--btn-icon:var(--color-zinc-400)] data-active:[--btn-icon:var(--color-zinc-300)] data-hover:[--btn-icon:var(--color-zinc-300)]',
|
|
89
|
+
],
|
|
90
|
+
indigo: [
|
|
91
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-indigo-500)] [--btn-border:var(--color-indigo-600)]/90',
|
|
92
|
+
'[--btn-icon:var(--color-indigo-300)] data-active:[--btn-icon:var(--color-indigo-200)] data-hover:[--btn-icon:var(--color-indigo-200)]',
|
|
93
|
+
],
|
|
94
|
+
cyan: [
|
|
95
|
+
'text-cyan-950 [--btn-bg:var(--color-cyan-300)] [--btn-border:var(--color-cyan-400)]/80 [--btn-hover-overlay:var(--color-white)]/25',
|
|
96
|
+
'[--btn-icon:var(--color-cyan-500)]',
|
|
97
|
+
],
|
|
98
|
+
red: [
|
|
99
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-red-600)] [--btn-border:var(--color-red-700)]/90',
|
|
100
|
+
'[--btn-icon:var(--color-red-300)] data-active:[--btn-icon:var(--color-red-200)] data-hover:[--btn-icon:var(--color-red-200)]',
|
|
101
|
+
],
|
|
102
|
+
orange: [
|
|
103
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-orange-500)] [--btn-border:var(--color-orange-600)]/90',
|
|
104
|
+
'[--btn-icon:var(--color-orange-300)] data-active:[--btn-icon:var(--color-orange-200)] data-hover:[--btn-icon:var(--color-orange-200)]',
|
|
105
|
+
],
|
|
106
|
+
amber: [
|
|
107
|
+
'text-amber-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-amber-400)] [--btn-border:var(--color-amber-500)]/80',
|
|
108
|
+
'[--btn-icon:var(--color-amber-600)]',
|
|
109
|
+
],
|
|
110
|
+
yellow: [
|
|
111
|
+
'text-yellow-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-yellow-300)] [--btn-border:var(--color-yellow-400)]/80',
|
|
112
|
+
'[--btn-icon:var(--color-yellow-600)] data-active:[--btn-icon:var(--color-yellow-700)] data-hover:[--btn-icon:var(--color-yellow-700)]',
|
|
113
|
+
],
|
|
114
|
+
lime: [
|
|
115
|
+
'text-lime-950 [--btn-hover-overlay:var(--color-white)]/25 [--btn-bg:var(--color-lime-300)] [--btn-border:var(--color-lime-400)]/80',
|
|
116
|
+
'[--btn-icon:var(--color-lime-600)] data-active:[--btn-icon:var(--color-lime-700)] data-hover:[--btn-icon:var(--color-lime-700)]',
|
|
117
|
+
],
|
|
118
|
+
green: [
|
|
119
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-green-600)] [--btn-border:var(--color-green-700)]/90',
|
|
120
|
+
'[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80',
|
|
121
|
+
],
|
|
122
|
+
emerald: [
|
|
123
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-emerald-600)] [--btn-border:var(--color-emerald-700)]/90',
|
|
124
|
+
'[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80',
|
|
125
|
+
],
|
|
126
|
+
teal: [
|
|
127
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-teal-600)] [--btn-border:var(--color-teal-700)]/90',
|
|
128
|
+
'[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80',
|
|
129
|
+
],
|
|
130
|
+
sky: [
|
|
131
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-sky-500)] [--btn-border:var(--color-sky-600)]/80',
|
|
132
|
+
'[--btn-icon:var(--color-white)]/60 data-active:[--btn-icon:var(--color-white)]/80 data-hover:[--btn-icon:var(--color-white)]/80',
|
|
133
|
+
],
|
|
134
|
+
blue: [
|
|
135
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-blue-600)] [--btn-border:var(--color-blue-700)]/90',
|
|
136
|
+
'[--btn-icon:var(--color-blue-400)] data-active:[--btn-icon:var(--color-blue-300)] data-hover:[--btn-icon:var(--color-blue-300)]',
|
|
137
|
+
],
|
|
138
|
+
violet: [
|
|
139
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-violet-500)] [--btn-border:var(--color-violet-600)]/90',
|
|
140
|
+
'[--btn-icon:var(--color-violet-300)] data-active:[--btn-icon:var(--color-violet-200)] data-hover:[--btn-icon:var(--color-violet-200)]',
|
|
141
|
+
],
|
|
142
|
+
purple: [
|
|
143
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-purple-500)] [--btn-border:var(--color-purple-600)]/90',
|
|
144
|
+
'[--btn-icon:var(--color-purple-300)] data-active:[--btn-icon:var(--color-purple-200)] data-hover:[--btn-icon:var(--color-purple-200)]',
|
|
145
|
+
],
|
|
146
|
+
fuchsia: [
|
|
147
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-fuchsia-500)] [--btn-border:var(--color-fuchsia-600)]/90',
|
|
148
|
+
'[--btn-icon:var(--color-fuchsia-300)] data-active:[--btn-icon:var(--color-fuchsia-200)] data-hover:[--btn-icon:var(--color-fuchsia-200)]',
|
|
149
|
+
],
|
|
150
|
+
pink: [
|
|
151
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-pink-500)] [--btn-border:var(--color-pink-600)]/90',
|
|
152
|
+
'[--btn-icon:var(--color-pink-300)] data-active:[--btn-icon:var(--color-pink-200)] data-hover:[--btn-icon:var(--color-pink-200)]',
|
|
153
|
+
],
|
|
154
|
+
rose: [
|
|
155
|
+
'text-white [--btn-hover-overlay:var(--color-white)]/10 [--btn-bg:var(--color-rose-500)] [--btn-border:var(--color-rose-600)]/90',
|
|
156
|
+
'[--btn-icon:var(--color-rose-300)] data-active:[--btn-icon:var(--color-rose-200)] data-hover:[--btn-icon:var(--color-rose-200)]',
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
type ButtonProps = (
|
|
162
|
+
| { color?: keyof typeof styles.colors; outline?: never; plain?: never }
|
|
163
|
+
| { color?: never; outline: true; plain?: never }
|
|
164
|
+
| { color?: never; outline?: never; plain: true }
|
|
165
|
+
) & { className?: string; children: React.ReactNode } & (
|
|
166
|
+
| Omit<Headless.ButtonProps, 'as' | 'className'>
|
|
167
|
+
| Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
export const Button = forwardRef(function Button(
|
|
171
|
+
{ color, outline, plain, className, children, ...props }: ButtonProps,
|
|
172
|
+
ref: React.ForwardedRef<HTMLElement>
|
|
173
|
+
) {
|
|
174
|
+
let classes = clsx(
|
|
175
|
+
className,
|
|
176
|
+
styles.base,
|
|
177
|
+
outline ? styles.outline : plain ? styles.plain : clsx(styles.solid, styles.colors[color ?? 'dark/zinc'])
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return 'href' in props ? (
|
|
181
|
+
<Link {...props} className={classes} ref={ref as React.ForwardedRef<HTMLAnchorElement>}>
|
|
182
|
+
<TouchTarget>{children}</TouchTarget>
|
|
183
|
+
</Link>
|
|
184
|
+
) : (
|
|
185
|
+
<Headless.Button {...props} className={clsx(classes, 'cursor-default')} ref={ref}>
|
|
186
|
+
<TouchTarget>{children}</TouchTarget>
|
|
187
|
+
</Headless.Button>
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Expand the hit area to at least 44×44px on touch devices
|
|
193
|
+
*/
|
|
194
|
+
export function TouchTarget({ children }: { children: React.ReactNode }) {
|
|
195
|
+
return (
|
|
196
|
+
<>
|
|
197
|
+
<span
|
|
198
|
+
className="absolute top-1/2 left-1/2 size-[max(100%,2.75rem)] -translate-x-1/2 -translate-y-1/2 pointer-fine:hidden"
|
|
199
|
+
aria-hidden="true"
|
|
200
|
+
/>
|
|
201
|
+
{children}
|
|
202
|
+
</>
|
|
203
|
+
)
|
|
204
|
+
}
|