@taicode/common-web 1.0.2 → 1.0.4

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.
@@ -0,0 +1,86 @@
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 Dialog({
19
+ size = 'lg',
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/25 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] justify-items-center 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 min-w-0 rounded-t-3xl bg-white p-(--gutter) shadow-lg ring-1 ring-zinc-950/10 [--gutter:--spacing(8)] sm:mb-auto sm:rounded-2xl dark:bg-zinc-900 dark:ring-white/10 forced-colors:outline',
42
+ 'transition duration-100 will-change-transform data-closed:translate-y-12 data-closed:opacity-0 data-enter:ease-out data-leave:ease-in sm:data-closed:translate-y-0 sm:data-closed:data-enter:scale-95'
43
+ )}
44
+ >
45
+ {children}
46
+ </Headless.DialogPanel>
47
+ </div>
48
+ </div>
49
+ </Headless.Dialog>
50
+ )
51
+ }
52
+
53
+ export function DialogTitle({
54
+ className,
55
+ ...props
56
+ }: { className?: string } & Omit<Headless.DialogTitleProps, 'as' | 'className'>) {
57
+ return (
58
+ <Headless.DialogTitle
59
+ {...props}
60
+ className={clsx(className, 'text-lg/6 font-semibold text-balance text-zinc-950 sm:text-base/6 dark:text-white')}
61
+ />
62
+ )
63
+ }
64
+
65
+ export function DialogDescription({
66
+ className,
67
+ ...props
68
+ }: { className?: string } & Omit<Headless.DescriptionProps<typeof Text>, 'as' | 'className'>) {
69
+ return <Headless.Description as={Text} {...props} className={clsx(className, 'mt-2 text-pretty')} />
70
+ }
71
+
72
+ export function DialogBody({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
73
+ return <div {...props} className={clsx(className, 'mt-6')} />
74
+ }
75
+
76
+ export function DialogActions({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
77
+ return (
78
+ <div
79
+ {...props}
80
+ className={clsx(
81
+ className,
82
+ 'mt-8 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:flex-row sm:*:w-auto'
83
+ )}
84
+ />
85
+ )
86
+ }
@@ -0,0 +1,20 @@
1
+ import clsx from 'clsx'
2
+
3
+ export function Divider({
4
+ soft = false,
5
+ className,
6
+ ...props
7
+ }: { soft?: boolean } & React.ComponentPropsWithoutRef<'hr'>) {
8
+ return (
9
+ <hr
10
+ role="presentation"
11
+ {...props}
12
+ className={clsx(
13
+ className,
14
+ 'w-full border-t',
15
+ soft && 'border-zinc-950/5 dark:border-white/5',
16
+ !soft && 'border-zinc-950/10 dark:border-white/10'
17
+ )}
18
+ />
19
+ )
20
+ }
@@ -0,0 +1,183 @@
1
+ 'use client'
2
+
3
+ import * as Headless from '@headlessui/react'
4
+ import clsx from 'clsx'
5
+ import type React from 'react'
6
+ import { Button } from './button'
7
+ import { Link } from './link'
8
+
9
+ export function Dropdown(props: Headless.MenuProps) {
10
+ return <Headless.Menu {...props} />
11
+ }
12
+
13
+ export function DropdownButton<T extends React.ElementType = typeof Button>({
14
+ as = Button,
15
+ ...props
16
+ }: { className?: string } & Omit<Headless.MenuButtonProps<T>, 'className'>) {
17
+ return <Headless.MenuButton as={as} {...props} />
18
+ }
19
+
20
+ export function DropdownMenu({
21
+ anchor = 'bottom',
22
+ className,
23
+ ...props
24
+ }: { className?: string } & Omit<Headless.MenuItemsProps, 'as' | 'className'>) {
25
+ return (
26
+ <Headless.MenuItems
27
+ {...props}
28
+ transition
29
+ anchor={anchor}
30
+ className={clsx(
31
+ className,
32
+ // Anchor positioning
33
+ '[--anchor-gap:--spacing(2)] [--anchor-padding:--spacing(1)] data-[anchor~=end]:[--anchor-offset:6px] data-[anchor~=start]:[--anchor-offset:-6px] sm:data-[anchor~=end]:[--anchor-offset:4px] sm:data-[anchor~=start]:[--anchor-offset:-4px]',
34
+ // Base styles
35
+ 'isolate w-max rounded-xl p-1',
36
+ // Invisible border that is only visible in `forced-colors` mode for accessibility purposes
37
+ 'outline outline-transparent focus:outline-hidden',
38
+ // Handle scrolling when menu won't fit in viewport
39
+ 'overflow-y-auto',
40
+ // Popover background
41
+ 'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
42
+ // Shadows
43
+ 'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset',
44
+ // Define grid at the menu level if subgrid is supported
45
+ 'supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]',
46
+ // Transitions
47
+ 'transition data-leave:duration-100 data-leave:ease-in data-closed:data-leave:opacity-0'
48
+ )}
49
+ />
50
+ )
51
+ }
52
+
53
+ export function DropdownItem({
54
+ className,
55
+ ...props
56
+ }: { className?: string } & (
57
+ | Omit<Headless.MenuItemProps<'button'>, 'as' | 'className'>
58
+ | Omit<Headless.MenuItemProps<typeof Link>, 'as' | 'className'>
59
+ )) {
60
+ let classes = clsx(
61
+ className,
62
+ // Base styles
63
+ 'group cursor-default rounded-lg px-3.5 py-2.5 focus:outline-hidden sm:px-3 sm:py-1.5',
64
+ // Text styles
65
+ 'text-left text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
66
+ // Focus
67
+ 'data-focus:bg-blue-500 data-focus:text-white',
68
+ // Disabled state
69
+ 'data-disabled:opacity-50',
70
+ // Forced colors mode
71
+ 'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText] forced-colors:data-focus:*:data-[slot=icon]:text-[HighlightText]',
72
+ // Use subgrid when available but fallback to an explicit grid layout if not
73
+ 'col-span-full grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] items-center supports-[grid-template-columns:subgrid]:grid-cols-subgrid',
74
+ // Icons
75
+ '*:data-[slot=icon]:col-start-1 *:data-[slot=icon]:row-start-1 *:data-[slot=icon]:mr-2.5 *:data-[slot=icon]:-ml-0.5 *:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:mr-2 sm:*:data-[slot=icon]:size-4',
76
+ '*:data-[slot=icon]:text-zinc-500 data-focus:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400 dark:data-focus:*:data-[slot=icon]:text-white',
77
+ // Avatar
78
+ '*:data-[slot=avatar]:mr-2.5 *:data-[slot=avatar]:-ml-1 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:mr-2 sm:*:data-[slot=avatar]:size-5'
79
+ )
80
+
81
+ return 'href' in props ? (
82
+ <Headless.MenuItem as={Link} {...props} className={classes} />
83
+ ) : (
84
+ <Headless.MenuItem as="button" type="button" {...props} className={classes} />
85
+ )
86
+ }
87
+
88
+ export function DropdownHeader({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
89
+ return <div {...props} className={clsx(className, 'col-span-5 px-3.5 pt-2.5 pb-1 sm:px-3')} />
90
+ }
91
+
92
+ export function DropdownSection({
93
+ className,
94
+ ...props
95
+ }: { className?: string } & Omit<Headless.MenuSectionProps, 'as' | 'className'>) {
96
+ return (
97
+ <Headless.MenuSection
98
+ {...props}
99
+ className={clsx(
100
+ className,
101
+ // Define grid at the section level instead of the item level if subgrid is supported
102
+ 'col-span-full supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]'
103
+ )}
104
+ />
105
+ )
106
+ }
107
+
108
+ export function DropdownHeading({
109
+ className,
110
+ ...props
111
+ }: { className?: string } & Omit<Headless.MenuHeadingProps, 'as' | 'className'>) {
112
+ return (
113
+ <Headless.MenuHeading
114
+ {...props}
115
+ className={clsx(
116
+ className,
117
+ 'col-span-full grid grid-cols-[1fr_auto] gap-x-12 px-3.5 pt-2 pb-1 text-sm/5 font-medium text-zinc-500 sm:px-3 sm:text-xs/5 dark:text-zinc-400'
118
+ )}
119
+ />
120
+ )
121
+ }
122
+
123
+ export function DropdownDivider({
124
+ className,
125
+ ...props
126
+ }: { className?: string } & Omit<Headless.MenuSeparatorProps, 'as' | 'className'>) {
127
+ return (
128
+ <Headless.MenuSeparator
129
+ {...props}
130
+ className={clsx(
131
+ className,
132
+ 'col-span-full mx-3.5 my-1 h-px border-0 bg-zinc-950/5 sm:mx-3 dark:bg-white/10 forced-colors:bg-[CanvasText]'
133
+ )}
134
+ />
135
+ )
136
+ }
137
+
138
+ export function DropdownLabel({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
139
+ return <div {...props} data-slot="label" className={clsx(className, 'col-start-2 row-start-1')} {...props} />
140
+ }
141
+
142
+ export function DropdownDescription({
143
+ className,
144
+ ...props
145
+ }: { className?: string } & Omit<Headless.DescriptionProps, 'as' | 'className'>) {
146
+ return (
147
+ <Headless.Description
148
+ data-slot="description"
149
+ {...props}
150
+ className={clsx(
151
+ className,
152
+ 'col-span-2 col-start-2 row-start-2 text-sm/5 text-zinc-500 group-data-focus:text-white sm:text-xs/5 dark:text-zinc-400 forced-colors:group-data-focus:text-[HighlightText]'
153
+ )}
154
+ />
155
+ )
156
+ }
157
+
158
+ export function DropdownShortcut({
159
+ keys,
160
+ className,
161
+ ...props
162
+ }: { keys: string | string[]; className?: string } & Omit<Headless.DescriptionProps<'kbd'>, 'as' | 'className'>) {
163
+ return (
164
+ <Headless.Description
165
+ as="kbd"
166
+ {...props}
167
+ className={clsx(className, 'col-start-5 row-start-1 flex justify-self-end')}
168
+ >
169
+ {(Array.isArray(keys) ? keys : keys.split('')).map((char, index) => (
170
+ <kbd
171
+ key={index}
172
+ className={clsx([
173
+ 'min-w-[2ch] text-center font-sans text-zinc-400 capitalize group-data-focus:text-white forced-colors:group-data-focus:text-[HighlightText]',
174
+ // Make sure key names that are longer than one character (like "Tab") have extra space
175
+ index > 0 && char.length > 1 && 'pl-1',
176
+ ])}
177
+ >
178
+ {char}
179
+ </kbd>
180
+ ))}
181
+ </Headless.Description>
182
+ )
183
+ }
@@ -0,0 +1,91 @@
1
+ import * as Headless from '@headlessui/react'
2
+ import clsx from 'clsx'
3
+ import type React from 'react'
4
+
5
+ export function Fieldset({
6
+ className,
7
+ ...props
8
+ }: { className?: string } & Omit<Headless.FieldsetProps, 'as' | 'className'>) {
9
+ return (
10
+ <Headless.Fieldset
11
+ {...props}
12
+ className={clsx(className, '*:data-[slot=text]:mt-1 [&>*+[data-slot=control]]:mt-6')}
13
+ />
14
+ )
15
+ }
16
+
17
+ export function Legend({
18
+ className,
19
+ ...props
20
+ }: { className?: string } & Omit<Headless.LegendProps, 'as' | 'className'>) {
21
+ return (
22
+ <Headless.Legend
23
+ data-slot="legend"
24
+ {...props}
25
+ className={clsx(
26
+ className,
27
+ 'text-base/6 font-semibold text-zinc-950 data-disabled:opacity-50 sm:text-sm/6 dark:text-white'
28
+ )}
29
+ />
30
+ )
31
+ }
32
+
33
+ export function FieldGroup({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
34
+ return <div data-slot="control" {...props} className={clsx(className, 'space-y-8')} />
35
+ }
36
+
37
+ export function Field({ className, ...props }: { className?: string } & Omit<Headless.FieldProps, 'as' | 'className'>) {
38
+ return (
39
+ <Headless.Field
40
+ {...props}
41
+ className={clsx(
42
+ className,
43
+ '[&>[data-slot=label]+[data-slot=control]]:mt-3',
44
+ '[&>[data-slot=label]+[data-slot=description]]:mt-1',
45
+ '[&>[data-slot=description]+[data-slot=control]]:mt-3',
46
+ '[&>[data-slot=control]+[data-slot=description]]:mt-3',
47
+ '[&>[data-slot=control]+[data-slot=error]]:mt-3',
48
+ '*:data-[slot=label]:font-medium'
49
+ )}
50
+ />
51
+ )
52
+ }
53
+
54
+ export function Label({ className, ...props }: { className?: string } & Omit<Headless.LabelProps, 'as' | 'className'>) {
55
+ return (
56
+ <Headless.Label
57
+ data-slot="label"
58
+ {...props}
59
+ className={clsx(
60
+ className,
61
+ 'text-base/6 text-zinc-950 select-none data-disabled:opacity-50 sm:text-sm/6 dark:text-white'
62
+ )}
63
+ />
64
+ )
65
+ }
66
+
67
+ export function Description({
68
+ className,
69
+ ...props
70
+ }: { className?: string } & Omit<Headless.DescriptionProps, 'as' | 'className'>) {
71
+ return (
72
+ <Headless.Description
73
+ data-slot="description"
74
+ {...props}
75
+ className={clsx(className, 'text-base/6 text-zinc-500 data-disabled:opacity-50 sm:text-sm/6 dark:text-zinc-400')}
76
+ />
77
+ )
78
+ }
79
+
80
+ export function ErrorMessage({
81
+ className,
82
+ ...props
83
+ }: { className?: string } & Omit<Headless.DescriptionProps, 'as' | 'className'>) {
84
+ return (
85
+ <Headless.Description
86
+ data-slot="error"
87
+ {...props}
88
+ className={clsx(className, 'text-base/6 text-red-600 data-disabled:opacity-50 sm:text-sm/6 dark:text-red-500')}
89
+ />
90
+ )
91
+ }
@@ -0,0 +1,27 @@
1
+ import clsx from 'clsx'
2
+
3
+ type HeadingProps = { level?: 1 | 2 | 3 | 4 | 5 | 6 } & React.ComponentPropsWithoutRef<
4
+ 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
5
+ >
6
+
7
+ export function Heading({ className, level = 1, ...props }: HeadingProps) {
8
+ let Element: `h${typeof level}` = `h${level}`
9
+
10
+ return (
11
+ <Element
12
+ {...props}
13
+ className={clsx(className, 'text-2xl/8 font-semibold text-zinc-950 sm:text-xl/8 dark:text-white')}
14
+ />
15
+ )
16
+ }
17
+
18
+ export function Subheading({ className, level = 2, ...props }: HeadingProps) {
19
+ let Element: `h${typeof level}` = `h${level}`
20
+
21
+ return (
22
+ <Element
23
+ {...props}
24
+ className={clsx(className, 'text-base/7 font-semibold text-zinc-950 sm:text-sm/6 dark:text-white')}
25
+ />
26
+ )
27
+ }
@@ -0,0 +1,94 @@
1
+ import * as Headless from '@headlessui/react'
2
+ import clsx from 'clsx'
3
+ import React, { forwardRef } from 'react'
4
+
5
+ export function InputGroup({ children }: React.ComponentPropsWithoutRef<'span'>) {
6
+ return (
7
+ <span
8
+ data-slot="control"
9
+ className={clsx(
10
+ 'relative isolate block',
11
+ 'has-[[data-slot=icon]:first-child]:[&_input]:pl-10 has-[[data-slot=icon]:last-child]:[&_input]:pr-10 sm:has-[[data-slot=icon]:first-child]:[&_input]:pl-8 sm:has-[[data-slot=icon]:last-child]:[&_input]:pr-8',
12
+ '*:data-[slot=icon]:pointer-events-none *:data-[slot=icon]:absolute *:data-[slot=icon]:top-3 *:data-[slot=icon]:z-10 *:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:top-2.5 sm:*:data-[slot=icon]:size-4',
13
+ '[&>[data-slot=icon]:first-child]:left-3 sm:[&>[data-slot=icon]:first-child]:left-2.5 [&>[data-slot=icon]:last-child]:right-3 sm:[&>[data-slot=icon]:last-child]:right-2.5',
14
+ '*:data-[slot=icon]:text-zinc-500 dark:*:data-[slot=icon]:text-zinc-400'
15
+ )}
16
+ >
17
+ {children}
18
+ </span>
19
+ )
20
+ }
21
+
22
+ const dateTypes = ['date', 'datetime-local', 'month', 'time', 'week']
23
+ type DateType = (typeof dateTypes)[number]
24
+
25
+ export const Input = forwardRef(function Input(
26
+ {
27
+ className,
28
+ ...props
29
+ }: {
30
+ className?: string
31
+ type?: 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | DateType
32
+ } & Omit<Headless.InputProps, 'as' | 'className'>,
33
+ ref: React.ForwardedRef<HTMLInputElement>
34
+ ) {
35
+ return (
36
+ <span
37
+ data-slot="control"
38
+ className={clsx([
39
+ className,
40
+ // Basic layout
41
+ 'relative block w-full',
42
+ // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
43
+ 'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
44
+ // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
45
+ 'dark:before:hidden',
46
+ // Focus ring
47
+ 'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-blue-500',
48
+ // Disabled state
49
+ 'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
50
+ // Invalid state
51
+ 'has-data-invalid:before:shadow-red-500/10',
52
+ ])}
53
+ >
54
+ <Headless.Input
55
+ ref={ref}
56
+ {...props}
57
+ className={clsx([
58
+ // Date classes
59
+ props.type &&
60
+ dateTypes.includes(props.type) && [
61
+ '[&::-webkit-datetime-edit-fields-wrapper]:p-0',
62
+ '[&::-webkit-date-and-time-value]:min-h-[1.5em]',
63
+ '[&::-webkit-datetime-edit]:inline-flex',
64
+ '[&::-webkit-datetime-edit]:p-0',
65
+ '[&::-webkit-datetime-edit-year-field]:p-0',
66
+ '[&::-webkit-datetime-edit-month-field]:p-0',
67
+ '[&::-webkit-datetime-edit-day-field]:p-0',
68
+ '[&::-webkit-datetime-edit-hour-field]:p-0',
69
+ '[&::-webkit-datetime-edit-minute-field]:p-0',
70
+ '[&::-webkit-datetime-edit-second-field]:p-0',
71
+ '[&::-webkit-datetime-edit-millisecond-field]:p-0',
72
+ '[&::-webkit-datetime-edit-meridiem-field]:p-0',
73
+ ],
74
+ // Basic layout
75
+ 'relative block w-full appearance-none rounded-lg 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)]',
76
+ // Typography
77
+ 'text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white',
78
+ // Border
79
+ 'border border-zinc-950/10 data-hover:border-zinc-950/20 dark:border-white/10 dark:data-hover:border-white/20',
80
+ // Background color
81
+ 'bg-transparent dark:bg-white/5',
82
+ // Hide default focus styles
83
+ 'focus:outline-hidden',
84
+ // Invalid state
85
+ 'data-invalid:border-red-500 data-invalid:data-hover:border-red-500 dark:data-invalid:border-red-500 dark:data-invalid:data-hover:border-red-500',
86
+ // Disabled state
87
+ 'data-disabled:border-zinc-950/20 dark:data-disabled:border-white/15 dark:data-disabled:bg-white/2.5 dark:data-hover:data-disabled:border-white/15',
88
+ // System icons
89
+ 'dark:scheme-dark',
90
+ ])}
91
+ />
92
+ </span>
93
+ )
94
+ })
@@ -0,0 +1,21 @@
1
+ /**
2
+ * TODO: Update this component to use your client-side framework's link
3
+ * component. We've provided examples of how to do this for Next.js, Remix, and
4
+ * Inertia.js in the Catalyst documentation:
5
+ *
6
+ * https://catalyst.tailwindui.com/docs#client-side-router-integration
7
+ */
8
+
9
+ import * as Headless from '@headlessui/react'
10
+ import React, { forwardRef } from 'react'
11
+
12
+ export const Link = forwardRef(function Link(
13
+ props: { href: string } & React.ComponentPropsWithoutRef<'a'>,
14
+ ref: React.ForwardedRef<HTMLAnchorElement>
15
+ ) {
16
+ return (
17
+ <Headless.DataInteractive>
18
+ <a {...props} ref={ref} />
19
+ </Headless.DataInteractive>
20
+ )
21
+ })