@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
|
@@ -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
|
+
}
|