@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.
@@ -0,0 +1,157 @@
1
+ import * as Headless from '@headlessui/react'
2
+ import clsx from 'clsx'
3
+ import type React from 'react'
4
+
5
+ export function CheckboxGroup({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
6
+ return (
7
+ <div
8
+ data-slot="control"
9
+ {...props}
10
+ className={clsx(
11
+ className,
12
+ // Basic groups
13
+ 'space-y-3',
14
+ // With descriptions
15
+ 'has-data-[slot=description]:space-y-6 has-data-[slot=description]:**:data-[slot=label]:font-medium'
16
+ )}
17
+ />
18
+ )
19
+ }
20
+
21
+ export function CheckboxField({
22
+ className,
23
+ ...props
24
+ }: { className?: string } & Omit<Headless.FieldProps, 'as' | 'className'>) {
25
+ return (
26
+ <Headless.Field
27
+ data-slot="field"
28
+ {...props}
29
+ className={clsx(
30
+ className,
31
+ // Base layout
32
+ 'grid grid-cols-[1.125rem_1fr] gap-x-4 gap-y-1 sm:grid-cols-[1rem_1fr]',
33
+ // Control layout
34
+ '*:data-[slot=control]:col-start-1 *:data-[slot=control]:row-start-1 *:data-[slot=control]:mt-0.75 sm:*:data-[slot=control]:mt-1',
35
+ // Label layout
36
+ '*:data-[slot=label]:col-start-2 *:data-[slot=label]:row-start-1',
37
+ // Description layout
38
+ '*:data-[slot=description]:col-start-2 *:data-[slot=description]:row-start-2',
39
+ // With description
40
+ 'has-data-[slot=description]:**:data-[slot=label]:font-medium'
41
+ )}
42
+ />
43
+ )
44
+ }
45
+
46
+ const base = [
47
+ // Basic layout
48
+ 'relative isolate flex size-4.5 items-center justify-center rounded-[0.3125rem] sm:size-4',
49
+ // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
50
+ 'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(0.3125rem-1px)] before:bg-white before:shadow-sm',
51
+ // Background color when checked
52
+ 'group-data-checked:before:bg-(--checkbox-checked-bg)',
53
+ // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
54
+ 'dark:before:hidden',
55
+ // Background color applied to control in dark mode
56
+ 'dark:bg-white/5 dark:group-data-checked:bg-(--checkbox-checked-bg)',
57
+ // Border
58
+ 'border border-zinc-950/15 group-data-checked:border-transparent group-data-hover:group-data-checked:border-transparent group-data-hover:border-zinc-950/30 group-data-checked:bg-(--checkbox-checked-border)',
59
+ 'dark:border-white/15 dark:group-data-checked:border-white/5 dark:group-data-hover:group-data-checked:border-white/5 dark:group-data-hover:border-white/30',
60
+ // Inner highlight shadow
61
+ 'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
62
+ 'dark:after:-inset-px dark:after:hidden dark:after:rounded-[0.3125rem] dark:group-data-checked:after:block',
63
+ // Focus ring
64
+ 'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-blue-500',
65
+ // Disabled state
66
+ 'group-data-disabled:opacity-50',
67
+ 'group-data-disabled:border-zinc-950/25 group-data-disabled:bg-zinc-950/5 group-data-disabled:[--checkbox-check:var(--color-zinc-950)]/50 group-data-disabled:before:bg-transparent',
68
+ 'dark:group-data-disabled:border-white/20 dark:group-data-disabled:bg-white/2.5 dark:group-data-disabled:[--checkbox-check:var(--color-white)]/50 dark:group-data-checked:group-data-disabled:after:hidden',
69
+ // Forced colors mode
70
+ 'forced-colors:[--checkbox-check:HighlightText] forced-colors:[--checkbox-checked-bg:Highlight] forced-colors:group-data-disabled:[--checkbox-check:Highlight]',
71
+ 'dark:forced-colors:[--checkbox-check:HighlightText] dark:forced-colors:[--checkbox-checked-bg:Highlight] dark:forced-colors:group-data-disabled:[--checkbox-check:Highlight]',
72
+ ]
73
+
74
+ const colors = {
75
+ 'dark/zinc': [
76
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
77
+ 'dark:[--checkbox-checked-bg:var(--color-zinc-600)]',
78
+ ],
79
+ 'dark/white': [
80
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
81
+ 'dark:[--checkbox-check:var(--color-zinc-900)] dark:[--checkbox-checked-bg:var(--color-white)] dark:[--checkbox-checked-border:var(--color-zinc-950)]/15',
82
+ ],
83
+ white:
84
+ '[--checkbox-check:var(--color-zinc-900)] [--checkbox-checked-bg:var(--color-white)] [--checkbox-checked-border:var(--color-zinc-950)]/15',
85
+ dark: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
86
+ zinc: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-600)] [--checkbox-checked-border:var(--color-zinc-700)]/90',
87
+ red: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-red-600)] [--checkbox-checked-border:var(--color-red-700)]/90',
88
+ orange:
89
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-orange-500)] [--checkbox-checked-border:var(--color-orange-600)]/90',
90
+ amber:
91
+ '[--checkbox-check:var(--color-amber-950)] [--checkbox-checked-bg:var(--color-amber-400)] [--checkbox-checked-border:var(--color-amber-500)]/80',
92
+ yellow:
93
+ '[--checkbox-check:var(--color-yellow-950)] [--checkbox-checked-bg:var(--color-yellow-300)] [--checkbox-checked-border:var(--color-yellow-400)]/80',
94
+ lime: '[--checkbox-check:var(--color-lime-950)] [--checkbox-checked-bg:var(--color-lime-300)] [--checkbox-checked-border:var(--color-lime-400)]/80',
95
+ green:
96
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-green-600)] [--checkbox-checked-border:var(--color-green-700)]/90',
97
+ emerald:
98
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-emerald-600)] [--checkbox-checked-border:var(--color-emerald-700)]/90',
99
+ teal: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-teal-600)] [--checkbox-checked-border:var(--color-teal-700)]/90',
100
+ cyan: '[--checkbox-check:var(--color-cyan-950)] [--checkbox-checked-bg:var(--color-cyan-300)] [--checkbox-checked-border:var(--color-cyan-400)]/80',
101
+ sky: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-sky-500)] [--checkbox-checked-border:var(--color-sky-600)]/80',
102
+ blue: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-blue-600)] [--checkbox-checked-border:var(--color-blue-700)]/90',
103
+ indigo:
104
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-indigo-500)] [--checkbox-checked-border:var(--color-indigo-600)]/90',
105
+ violet:
106
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-violet-500)] [--checkbox-checked-border:var(--color-violet-600)]/90',
107
+ purple:
108
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-purple-500)] [--checkbox-checked-border:var(--color-purple-600)]/90',
109
+ fuchsia:
110
+ '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-fuchsia-500)] [--checkbox-checked-border:var(--color-fuchsia-600)]/90',
111
+ pink: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-pink-500)] [--checkbox-checked-border:var(--color-pink-600)]/90',
112
+ rose: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-rose-500)] [--checkbox-checked-border:var(--color-rose-600)]/90',
113
+ }
114
+
115
+ type Color = keyof typeof colors
116
+
117
+ export function Checkbox({
118
+ color = 'dark/zinc',
119
+ className,
120
+ ...props
121
+ }: {
122
+ color?: Color
123
+ className?: string
124
+ } & Omit<Headless.CheckboxProps, 'as' | 'className'>) {
125
+ return (
126
+ <Headless.Checkbox
127
+ data-slot="control"
128
+ {...props}
129
+ className={clsx(className, 'group inline-flex focus:outline-hidden')}
130
+ >
131
+ <span className={clsx([base, colors[color]])}>
132
+ <svg
133
+ className="size-4 stroke-(--checkbox-check) opacity-0 group-data-checked:opacity-100 sm:h-3.5 sm:w-3.5"
134
+ viewBox="0 0 14 14"
135
+ fill="none"
136
+ >
137
+ {/* Checkmark icon */}
138
+ <path
139
+ className="opacity-100 group-data-indeterminate:opacity-0"
140
+ d="M3 8L6 11L11 3.5"
141
+ strokeWidth={2}
142
+ strokeLinecap="round"
143
+ strokeLinejoin="round"
144
+ />
145
+ {/* Indeterminate icon */}
146
+ <path
147
+ className="opacity-0 group-data-indeterminate:opacity-100"
148
+ d="M3 7H11"
149
+ strokeWidth={2}
150
+ strokeLinecap="round"
151
+ strokeLinejoin="round"
152
+ />
153
+ </svg>
154
+ </span>
155
+ </Headless.Checkbox>
156
+ )
157
+ }
@@ -0,0 +1,188 @@
1
+ 'use client'
2
+
3
+ import * as Headless from '@headlessui/react'
4
+ import clsx from 'clsx'
5
+ import { useState } from 'react'
6
+
7
+ export function Combobox<T>({
8
+ options,
9
+ displayValue,
10
+ filter,
11
+ anchor = 'bottom',
12
+ className,
13
+ placeholder,
14
+ autoFocus,
15
+ 'aria-label': ariaLabel,
16
+ children,
17
+ ...props
18
+ }: {
19
+ options: T[]
20
+ displayValue: (value: T | null) => string | undefined
21
+ filter?: (value: T, query: string) => boolean
22
+ className?: string
23
+ placeholder?: string
24
+ autoFocus?: boolean
25
+ 'aria-label'?: string
26
+ children: (value: NonNullable<T>) => React.ReactElement
27
+ } & Omit<Headless.ComboboxProps<T, false>, 'as' | 'multiple' | 'children'> & { anchor?: 'top' | 'bottom' }) {
28
+ const [query, setQuery] = useState('')
29
+
30
+ const filteredOptions =
31
+ query === ''
32
+ ? options
33
+ : options.filter((option) =>
34
+ filter ? filter(option, query) : displayValue(option)?.toLowerCase().includes(query.toLowerCase())
35
+ )
36
+
37
+ return (
38
+ <Headless.Combobox {...props} multiple={false} virtual={{ options: filteredOptions }} onClose={() => setQuery('')}>
39
+ <span
40
+ data-slot="control"
41
+ className={clsx([
42
+ className,
43
+ // Basic layout
44
+ 'relative block w-full',
45
+ // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
46
+ 'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
47
+ // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
48
+ 'dark:before:hidden',
49
+ // Focus ring
50
+ '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',
51
+ // Disabled state
52
+ 'has-data-disabled:opacity-50 has-data-disabled:before:bg-zinc-950/5 has-data-disabled:before:shadow-none',
53
+ // Invalid state
54
+ 'has-data-invalid:before:shadow-red-500/10',
55
+ ])}
56
+ >
57
+ <Headless.ComboboxInput
58
+ autoFocus={autoFocus}
59
+ data-slot="control"
60
+ aria-label={ariaLabel}
61
+ displayValue={(option: T) => displayValue(option) ?? ''}
62
+ onChange={(event) => setQuery(event.target.value)}
63
+ placeholder={placeholder}
64
+ className={clsx([
65
+ className,
66
+ // Basic layout
67
+ 'relative block w-full appearance-none rounded-lg py-[calc(--spacing(2.5)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
68
+ // Horizontal padding
69
+ 'pr-[calc(--spacing(10)-1px)] pl-[calc(--spacing(3.5)-1px)] sm:pr-[calc(--spacing(9)-1px)] sm:pl-[calc(--spacing(3)-1px)]',
70
+ // Typography
71
+ 'text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white',
72
+ // Border
73
+ 'border border-zinc-950/10 data-hover:border-zinc-950/20 dark:border-white/10 dark:data-hover:border-white/20',
74
+ // Background color
75
+ 'bg-transparent dark:bg-white/5',
76
+ // Hide default focus styles
77
+ 'focus:outline-hidden',
78
+ // Invalid state
79
+ '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',
80
+ // Disabled state
81
+ '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',
82
+ // System icons
83
+ 'dark:scheme-dark',
84
+ ])}
85
+ />
86
+ <Headless.ComboboxButton className="group absolute inset-y-0 right-0 flex items-center px-2">
87
+ <svg
88
+ className="size-5 stroke-zinc-500 group-data-disabled:stroke-zinc-600 group-data-hover:stroke-zinc-700 sm:size-4 dark:stroke-zinc-400 dark:group-data-hover:stroke-zinc-300 forced-colors:stroke-[CanvasText]"
89
+ viewBox="0 0 16 16"
90
+ aria-hidden="true"
91
+ fill="none"
92
+ >
93
+ <path d="M5.75 10.75L8 13L10.25 10.75" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
94
+ <path d="M10.25 5.25L8 3L5.75 5.25" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
95
+ </svg>
96
+ </Headless.ComboboxButton>
97
+ </span>
98
+ <Headless.ComboboxOptions
99
+ transition
100
+ anchor={anchor}
101
+ className={clsx(
102
+ // Anchor positioning
103
+ '[--anchor-gap:--spacing(2)] [--anchor-padding:--spacing(4)] sm:data-[anchor~=start]:[--anchor-offset:-4px]',
104
+ // Base styles,
105
+ 'isolate min-w-[calc(var(--input-width)+8px)] scroll-py-1 rounded-xl p-1 select-none empty:invisible',
106
+ // Invisible border that is only visible in `forced-colors` mode for accessibility purposes
107
+ 'outline outline-transparent focus:outline-hidden',
108
+ // Handle scrolling when menu won't fit in viewport
109
+ 'overflow-y-scroll overscroll-contain',
110
+ // Popover background
111
+ 'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
112
+ // Shadows
113
+ 'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset',
114
+ // Transitions
115
+ 'transition-opacity duration-100 ease-in data-closed:data-leave:opacity-0 data-transition:pointer-events-none'
116
+ )}
117
+ >
118
+ {({ option }) => children(option)}
119
+ </Headless.ComboboxOptions>
120
+ </Headless.Combobox>
121
+ )
122
+ }
123
+
124
+ export function ComboboxOption<T>({
125
+ children,
126
+ className,
127
+ ...props
128
+ }: { className?: string; children?: React.ReactNode } & Omit<
129
+ Headless.ComboboxOptionProps<'div', T>,
130
+ 'as' | 'className'
131
+ >) {
132
+ let sharedClasses = clsx(
133
+ // Base
134
+ 'flex min-w-0 items-center',
135
+ // Icons
136
+ '*:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 sm:*:data-[slot=icon]:size-4',
137
+ '*:data-[slot=icon]:text-zinc-500 group-data-focus/option:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400',
138
+ 'forced-colors:*:data-[slot=icon]:text-[CanvasText] forced-colors:group-data-focus/option:*:data-[slot=icon]:text-[Canvas]',
139
+ // Avatars
140
+ '*:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:size-5'
141
+ )
142
+
143
+ return (
144
+ <Headless.ComboboxOption
145
+ {...props}
146
+ className={clsx(
147
+ // Basic layout
148
+ 'group/option grid w-full cursor-default grid-cols-[1fr_--spacing(5)] items-baseline gap-x-2 rounded-lg py-2.5 pr-2 pl-3.5 sm:grid-cols-[1fr_--spacing(4)] sm:py-1.5 sm:pr-2 sm:pl-3',
149
+ // Typography
150
+ 'text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
151
+ // Focus
152
+ 'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
153
+ // Forced colors mode
154
+ 'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
155
+ // Disabled
156
+ 'data-disabled:opacity-50'
157
+ )}
158
+ >
159
+ <span className={clsx(className, sharedClasses)}>{children}</span>
160
+ <svg
161
+ className="relative col-start-2 hidden size-5 self-center stroke-current group-data-selected/option:inline sm:size-4"
162
+ viewBox="0 0 16 16"
163
+ fill="none"
164
+ aria-hidden="true"
165
+ >
166
+ <path d="M4 8.5l3 3L12 4" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
167
+ </svg>
168
+ </Headless.ComboboxOption>
169
+ )
170
+ }
171
+
172
+ export function ComboboxLabel({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) {
173
+ return <span {...props} className={clsx(className, 'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0')} />
174
+ }
175
+
176
+ export function ComboboxDescription({ className, children, ...props }: React.ComponentPropsWithoutRef<'span'>) {
177
+ return (
178
+ <span
179
+ {...props}
180
+ className={clsx(
181
+ className,
182
+ 'flex flex-1 overflow-hidden text-zinc-500 group-data-focus/option:text-white before:w-2 before:min-w-0 before:shrink dark:text-zinc-400'
183
+ )}
184
+ >
185
+ <span className="flex-1 truncate">{children}</span>
186
+ </span>
187
+ )
188
+ }
@@ -0,0 +1,37 @@
1
+ import clsx from 'clsx'
2
+
3
+ export function DescriptionList({ className, ...props }: React.ComponentPropsWithoutRef<'dl'>) {
4
+ return (
5
+ <dl
6
+ {...props}
7
+ className={clsx(
8
+ className,
9
+ 'grid grid-cols-1 text-base/6 sm:grid-cols-[min(50%,--spacing(80))_auto] sm:text-sm/6'
10
+ )}
11
+ />
12
+ )
13
+ }
14
+
15
+ export function DescriptionTerm({ className, ...props }: React.ComponentPropsWithoutRef<'dt'>) {
16
+ return (
17
+ <dt
18
+ {...props}
19
+ className={clsx(
20
+ className,
21
+ 'col-start-1 border-t border-zinc-950/5 pt-3 text-zinc-500 first:border-none sm:border-t sm:border-zinc-950/5 sm:py-3 dark:border-white/5 dark:text-zinc-400 sm:dark:border-white/5'
22
+ )}
23
+ />
24
+ )
25
+ }
26
+
27
+ export function DescriptionDetails({ className, ...props }: React.ComponentPropsWithoutRef<'dd'>) {
28
+ return (
29
+ <dd
30
+ {...props}
31
+ className={clsx(
32
+ className,
33
+ 'pt-1 pb-3 text-zinc-950 sm:border-t sm:border-zinc-950/5 sm:py-3 sm:nth-2:border-none dark:text-white dark:sm:border-white/5'
34
+ )}
35
+ />
36
+ )
37
+ }
@@ -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,188 @@
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({
139
+ className,
140
+ ...props
141
+ }: { className?: string } & Omit<Headless.LabelProps, 'as' | 'className'>) {
142
+ return (
143
+ <Headless.Label {...props} data-slot="label" className={clsx(className, 'col-start-2 row-start-1')} {...props} />
144
+ )
145
+ }
146
+
147
+ export function DropdownDescription({
148
+ className,
149
+ ...props
150
+ }: { className?: string } & Omit<Headless.DescriptionProps, 'as' | 'className'>) {
151
+ return (
152
+ <Headless.Description
153
+ data-slot="description"
154
+ {...props}
155
+ className={clsx(
156
+ className,
157
+ '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]'
158
+ )}
159
+ />
160
+ )
161
+ }
162
+
163
+ export function DropdownShortcut({
164
+ keys,
165
+ className,
166
+ ...props
167
+ }: { keys: string | string[]; className?: string } & Omit<Headless.DescriptionProps<'kbd'>, 'as' | 'className'>) {
168
+ return (
169
+ <Headless.Description
170
+ as="kbd"
171
+ {...props}
172
+ className={clsx(className, 'col-start-5 row-start-1 flex justify-self-end')}
173
+ >
174
+ {(Array.isArray(keys) ? keys : keys.split('')).map((char, index) => (
175
+ <kbd
176
+ key={index}
177
+ className={clsx([
178
+ 'min-w-[2ch] text-center font-sans text-zinc-400 capitalize group-data-focus:text-white forced-colors:group-data-focus:text-[HighlightText]',
179
+ // Make sure key names that are longer than one character (like "Tab") have extra space
180
+ index > 0 && char.length > 1 && 'pl-1',
181
+ ])}
182
+ >
183
+ {char}
184
+ </kbd>
185
+ ))}
186
+ </Headless.Description>
187
+ )
188
+ }