@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,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
|
+
})
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as Headless from '@headlessui/react'
|
|
4
|
+
import clsx from 'clsx'
|
|
5
|
+
import { Fragment } from 'react'
|
|
6
|
+
|
|
7
|
+
export function Listbox<T>({
|
|
8
|
+
className,
|
|
9
|
+
placeholder,
|
|
10
|
+
autoFocus,
|
|
11
|
+
'aria-label': ariaLabel,
|
|
12
|
+
children: options,
|
|
13
|
+
...props
|
|
14
|
+
}: {
|
|
15
|
+
className?: string
|
|
16
|
+
placeholder?: React.ReactNode
|
|
17
|
+
autoFocus?: boolean
|
|
18
|
+
'aria-label'?: string
|
|
19
|
+
children?: React.ReactNode
|
|
20
|
+
} & Omit<Headless.ListboxProps<typeof Fragment, T>, 'as' | 'multiple'>) {
|
|
21
|
+
return (
|
|
22
|
+
<Headless.Listbox {...props} multiple={false}>
|
|
23
|
+
<Headless.ListboxButton
|
|
24
|
+
autoFocus={autoFocus}
|
|
25
|
+
data-slot="control"
|
|
26
|
+
aria-label={ariaLabel}
|
|
27
|
+
className={clsx([
|
|
28
|
+
className,
|
|
29
|
+
// Basic layout
|
|
30
|
+
'group relative block w-full',
|
|
31
|
+
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
|
|
32
|
+
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)] before:bg-white before:shadow-sm',
|
|
33
|
+
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
|
|
34
|
+
'dark:before:hidden',
|
|
35
|
+
// Hide default focus styles
|
|
36
|
+
'focus:outline-hidden',
|
|
37
|
+
// Focus ring
|
|
38
|
+
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset data-focus:after:ring-2 data-focus:after:ring-blue-500',
|
|
39
|
+
// Disabled state
|
|
40
|
+
'data-disabled:opacity-50 data-disabled:before:bg-zinc-950/5 data-disabled:before:shadow-none',
|
|
41
|
+
])}
|
|
42
|
+
>
|
|
43
|
+
<Headless.ListboxSelectedOption
|
|
44
|
+
as="span"
|
|
45
|
+
options={options}
|
|
46
|
+
placeholder={placeholder && <span className="block truncate text-zinc-500">{placeholder}</span>}
|
|
47
|
+
className={clsx([
|
|
48
|
+
// Basic layout
|
|
49
|
+
'relative block w-full appearance-none rounded-lg py-[calc(--spacing(2.5)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
|
|
50
|
+
// Set minimum height for when no value is selected
|
|
51
|
+
'min-h-11 sm:min-h-9',
|
|
52
|
+
// Horizontal padding
|
|
53
|
+
'pr-[calc(--spacing(7)-1px)] pl-[calc(--spacing(3.5)-1px)] sm:pl-[calc(--spacing(3)-1px)]',
|
|
54
|
+
// Typography
|
|
55
|
+
'text-left text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
|
|
56
|
+
// Border
|
|
57
|
+
'border border-zinc-950/10 group-data-active:border-zinc-950/20 group-data-hover:border-zinc-950/20 dark:border-white/10 dark:group-data-active:border-white/20 dark:group-data-hover:border-white/20',
|
|
58
|
+
// Background color
|
|
59
|
+
'bg-transparent dark:bg-white/5',
|
|
60
|
+
// Invalid state
|
|
61
|
+
'group-data-invalid:border-red-500 group-data-hover:group-data-invalid:border-red-500 dark:group-data-invalid:border-red-600 dark:data-hover:group-data-invalid:border-red-600',
|
|
62
|
+
// Disabled state
|
|
63
|
+
'group-data-disabled:border-zinc-950/20 group-data-disabled:opacity-100 dark:group-data-disabled:border-white/15 dark:group-data-disabled:bg-white/2.5 dark:group-data-disabled:data-hover:border-white/15',
|
|
64
|
+
])}
|
|
65
|
+
/>
|
|
66
|
+
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
|
67
|
+
<svg
|
|
68
|
+
className="size-5 stroke-zinc-500 group-data-disabled:stroke-zinc-600 sm:size-4 dark:stroke-zinc-400 forced-colors:stroke-[CanvasText]"
|
|
69
|
+
viewBox="0 0 16 16"
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
fill="none"
|
|
72
|
+
>
|
|
73
|
+
<path d="M5.75 10.75L8 13L10.25 10.75" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
|
74
|
+
<path d="M10.25 5.25L8 3L5.75 5.25" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
|
75
|
+
</svg>
|
|
76
|
+
</span>
|
|
77
|
+
</Headless.ListboxButton>
|
|
78
|
+
<Headless.ListboxOptions
|
|
79
|
+
transition
|
|
80
|
+
anchor="selection start"
|
|
81
|
+
className={clsx(
|
|
82
|
+
// Anchor positioning
|
|
83
|
+
'[--anchor-offset:-1.625rem] [--anchor-padding:--spacing(4)] sm:[--anchor-offset:-1.375rem]',
|
|
84
|
+
// Base styles
|
|
85
|
+
'isolate w-max min-w-[calc(var(--button-width)+1.75rem)] scroll-py-1 rounded-xl p-1 select-none',
|
|
86
|
+
// Invisible border that is only visible in `forced-colors` mode for accessibility purposes
|
|
87
|
+
'outline outline-transparent focus:outline-hidden',
|
|
88
|
+
// Handle scrolling when menu won't fit in viewport
|
|
89
|
+
'overflow-y-scroll overscroll-contain',
|
|
90
|
+
// Popover background
|
|
91
|
+
'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
|
|
92
|
+
// Shadows
|
|
93
|
+
'shadow-lg ring-1 ring-zinc-950/10 dark:ring-white/10 dark:ring-inset',
|
|
94
|
+
// Transitions
|
|
95
|
+
'transition-opacity duration-100 ease-in data-closed:data-leave:opacity-0 data-transition:pointer-events-none'
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{options}
|
|
99
|
+
</Headless.ListboxOptions>
|
|
100
|
+
</Headless.Listbox>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function ListboxOption<T>({
|
|
105
|
+
children,
|
|
106
|
+
className,
|
|
107
|
+
...props
|
|
108
|
+
}: { className?: string; children?: React.ReactNode } & Omit<
|
|
109
|
+
Headless.ListboxOptionProps<'div', T>,
|
|
110
|
+
'as' | 'className'
|
|
111
|
+
>) {
|
|
112
|
+
let sharedClasses = clsx(
|
|
113
|
+
// Base
|
|
114
|
+
'flex min-w-0 items-center',
|
|
115
|
+
// Icons
|
|
116
|
+
'*:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 sm:*:data-[slot=icon]:size-4',
|
|
117
|
+
'*:data-[slot=icon]:text-zinc-500 group-data-focus/option:*:data-[slot=icon]:text-white dark:*:data-[slot=icon]:text-zinc-400',
|
|
118
|
+
'forced-colors:*:data-[slot=icon]:text-[CanvasText] forced-colors:group-data-focus/option:*:data-[slot=icon]:text-[Canvas]',
|
|
119
|
+
// Avatars
|
|
120
|
+
'*:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:size-5'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Headless.ListboxOption as={Fragment} {...props}>
|
|
125
|
+
{({ selectedOption }) => {
|
|
126
|
+
if (selectedOption) {
|
|
127
|
+
return <div className={clsx(className, sharedClasses)}>{children}</div>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
className={clsx(
|
|
133
|
+
// Basic layout
|
|
134
|
+
'group/option grid cursor-default grid-cols-[--spacing(5)_1fr] items-baseline gap-x-2 rounded-lg py-2.5 pr-3.5 pl-2 sm:grid-cols-[--spacing(4)_1fr] sm:py-1.5 sm:pr-3 sm:pl-1.5',
|
|
135
|
+
// Typography
|
|
136
|
+
'text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
|
|
137
|
+
// Focus
|
|
138
|
+
'outline-hidden data-focus:bg-blue-500 data-focus:text-white',
|
|
139
|
+
// Forced colors mode
|
|
140
|
+
'forced-color-adjust-none forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText]',
|
|
141
|
+
// Disabled
|
|
142
|
+
'data-disabled:opacity-50'
|
|
143
|
+
)}
|
|
144
|
+
>
|
|
145
|
+
<svg
|
|
146
|
+
className="relative hidden size-5 self-center stroke-current group-data-selected/option:inline sm:size-4"
|
|
147
|
+
viewBox="0 0 16 16"
|
|
148
|
+
fill="none"
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
>
|
|
151
|
+
<path d="M4 8.5l3 3L12 4" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
|
152
|
+
</svg>
|
|
153
|
+
<span className={clsx(className, sharedClasses, 'col-start-2')}>{children}</span>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}}
|
|
157
|
+
</Headless.ListboxOption>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function ListboxLabel({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) {
|
|
162
|
+
return <span {...props} className={clsx(className, 'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0')} />
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function ListboxDescription({ className, children, ...props }: React.ComponentPropsWithoutRef<'span'>) {
|
|
166
|
+
return (
|
|
167
|
+
<span
|
|
168
|
+
{...props}
|
|
169
|
+
className={clsx(
|
|
170
|
+
className,
|
|
171
|
+
'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'
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
<span className="flex-1 truncate">{children}</span>
|
|
175
|
+
</span>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as Headless from '@headlessui/react'
|
|
4
|
+
import clsx from 'clsx'
|
|
5
|
+
import { LayoutGroup, motion } from 'framer-motion'
|
|
6
|
+
import React, { forwardRef, useId } from 'react'
|
|
7
|
+
import { TouchTarget } from './button'
|
|
8
|
+
import { Link } from './link'
|
|
9
|
+
|
|
10
|
+
export function Navbar({ className, ...props }: React.ComponentPropsWithoutRef<'nav'>) {
|
|
11
|
+
return <nav {...props} className={clsx(className, 'flex flex-1 items-center gap-4 py-2.5')} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function NavbarDivider({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
|
15
|
+
return <div aria-hidden="true" {...props} className={clsx(className, 'h-6 w-px bg-zinc-950/10 dark:bg-white/10')} />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function NavbarSection({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
|
19
|
+
let id = useId()
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<LayoutGroup id={id}>
|
|
23
|
+
<div {...props} className={clsx(className, 'flex items-center gap-3')} />
|
|
24
|
+
</LayoutGroup>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function NavbarSpacer({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
|
29
|
+
return <div aria-hidden="true" {...props} className={clsx(className, '-ml-4 flex-1')} />
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const NavbarItem = forwardRef(function NavbarItem(
|
|
33
|
+
{
|
|
34
|
+
current,
|
|
35
|
+
className,
|
|
36
|
+
children,
|
|
37
|
+
...props
|
|
38
|
+
}: { current?: boolean; className?: string; children: React.ReactNode } & (
|
|
39
|
+
| Omit<Headless.ButtonProps, 'as' | 'className'>
|
|
40
|
+
| Omit<React.ComponentPropsWithoutRef<typeof Link>, 'className'>
|
|
41
|
+
),
|
|
42
|
+
ref: React.ForwardedRef<HTMLAnchorElement | HTMLButtonElement>
|
|
43
|
+
) {
|
|
44
|
+
let classes = clsx(
|
|
45
|
+
// Base
|
|
46
|
+
'relative flex min-w-0 items-center gap-3 rounded-lg p-2 text-left text-base/6 font-medium text-zinc-950 sm:text-sm/5',
|
|
47
|
+
// Leading icon/icon-only
|
|
48
|
+
'*:data-[slot=icon]:size-6 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:fill-zinc-500 sm:*:data-[slot=icon]:size-5',
|
|
49
|
+
// Trailing icon (down chevron or similar)
|
|
50
|
+
'*:not-nth-2:last:data-[slot=icon]:ml-auto *:not-nth-2:last:data-[slot=icon]:size-5 sm:*:not-nth-2:last:data-[slot=icon]:size-4',
|
|
51
|
+
// Avatar
|
|
52
|
+
'*:data-[slot=avatar]:-m-0.5 *:data-[slot=avatar]:size-7 *:data-[slot=avatar]:[--avatar-radius:var(--radius-md)] sm:*:data-[slot=avatar]:size-6',
|
|
53
|
+
// Hover
|
|
54
|
+
'data-hover:bg-zinc-950/5 data-hover:*:data-[slot=icon]:fill-zinc-950',
|
|
55
|
+
// Active
|
|
56
|
+
'data-active:bg-zinc-950/5 data-active:*:data-[slot=icon]:fill-zinc-950',
|
|
57
|
+
// Dark mode
|
|
58
|
+
'dark:text-white dark:*:data-[slot=icon]:fill-zinc-400',
|
|
59
|
+
'dark:data-hover:bg-white/5 dark:data-hover:*:data-[slot=icon]:fill-white',
|
|
60
|
+
'dark:data-active:bg-white/5 dark:data-active:*:data-[slot=icon]:fill-white'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<span className={clsx(className, 'relative')}>
|
|
65
|
+
{current && (
|
|
66
|
+
<motion.span
|
|
67
|
+
layoutId="current-indicator"
|
|
68
|
+
className="absolute inset-x-2 -bottom-2.5 h-0.5 rounded-full bg-zinc-950 dark:bg-white"
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
{'href' in props ? (
|
|
72
|
+
<Link
|
|
73
|
+
{...props}
|
|
74
|
+
className={classes}
|
|
75
|
+
data-current={current ? 'true' : undefined}
|
|
76
|
+
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
|
|
77
|
+
>
|
|
78
|
+
<TouchTarget>{children}</TouchTarget>
|
|
79
|
+
</Link>
|
|
80
|
+
) : (
|
|
81
|
+
<Headless.Button
|
|
82
|
+
{...props}
|
|
83
|
+
className={clsx('cursor-default', classes)}
|
|
84
|
+
data-current={current ? 'true' : undefined}
|
|
85
|
+
ref={ref}
|
|
86
|
+
>
|
|
87
|
+
<TouchTarget>{children}</TouchTarget>
|
|
88
|
+
</Headless.Button>
|
|
89
|
+
)}
|
|
90
|
+
</span>
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export function NavbarLabel({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) {
|
|
95
|
+
return <span {...props} className={clsx(className, 'truncate')} />
|
|
96
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import type React from 'react'
|
|
3
|
+
import { Button } from './button'
|
|
4
|
+
|
|
5
|
+
export function Pagination({
|
|
6
|
+
'aria-label': ariaLabel = 'Page navigation',
|
|
7
|
+
className,
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentPropsWithoutRef<'nav'>) {
|
|
10
|
+
return <nav aria-label={ariaLabel} {...props} className={clsx(className, 'flex gap-x-2')} />
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PaginationPrevious({
|
|
14
|
+
href = null,
|
|
15
|
+
className,
|
|
16
|
+
children = 'Previous',
|
|
17
|
+
}: React.PropsWithChildren<{ href?: string | null; className?: string }>) {
|
|
18
|
+
return (
|
|
19
|
+
<span className={clsx(className, 'grow basis-0')}>
|
|
20
|
+
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Previous page">
|
|
21
|
+
<svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
22
|
+
<path
|
|
23
|
+
d="M2.75 8H13.25M2.75 8L5.25 5.5M2.75 8L5.25 10.5"
|
|
24
|
+
strokeWidth={1.5}
|
|
25
|
+
strokeLinecap="round"
|
|
26
|
+
strokeLinejoin="round"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
29
|
+
{children}
|
|
30
|
+
</Button>
|
|
31
|
+
</span>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function PaginationNext({
|
|
36
|
+
href = null,
|
|
37
|
+
className,
|
|
38
|
+
children = 'Next',
|
|
39
|
+
}: React.PropsWithChildren<{ href?: string | null; className?: string }>) {
|
|
40
|
+
return (
|
|
41
|
+
<span className={clsx(className, 'flex grow basis-0 justify-end')}>
|
|
42
|
+
<Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Next page">
|
|
43
|
+
{children}
|
|
44
|
+
<svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
45
|
+
<path
|
|
46
|
+
d="M13.25 8L2.75 8M13.25 8L10.75 10.5M13.25 8L10.75 5.5"
|
|
47
|
+
strokeWidth={1.5}
|
|
48
|
+
strokeLinecap="round"
|
|
49
|
+
strokeLinejoin="round"
|
|
50
|
+
/>
|
|
51
|
+
</svg>
|
|
52
|
+
</Button>
|
|
53
|
+
</span>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function PaginationList({ className, ...props }: React.ComponentPropsWithoutRef<'span'>) {
|
|
58
|
+
return <span {...props} className={clsx(className, 'hidden items-baseline gap-x-2 sm:flex')} />
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function PaginationPage({
|
|
62
|
+
href,
|
|
63
|
+
className,
|
|
64
|
+
current = false,
|
|
65
|
+
children,
|
|
66
|
+
}: React.PropsWithChildren<{ href: string; className?: string; current?: boolean }>) {
|
|
67
|
+
return (
|
|
68
|
+
<Button
|
|
69
|
+
href={href}
|
|
70
|
+
plain
|
|
71
|
+
aria-label={`Page ${children}`}
|
|
72
|
+
aria-current={current ? 'page' : undefined}
|
|
73
|
+
className={clsx(
|
|
74
|
+
className,
|
|
75
|
+
'min-w-9 before:absolute before:-inset-px before:rounded-lg',
|
|
76
|
+
current && 'before:bg-zinc-950/5 dark:before:bg-white/10'
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
<span className="-mx-0.5">{children}</span>
|
|
80
|
+
</Button>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function PaginationGap({
|
|
85
|
+
className,
|
|
86
|
+
children = <>…</>,
|
|
87
|
+
...props
|
|
88
|
+
}: React.ComponentPropsWithoutRef<'span'>) {
|
|
89
|
+
return (
|
|
90
|
+
<span
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
{...props}
|
|
93
|
+
className={clsx(className, 'w-9 text-center text-sm/6 font-semibold text-zinc-950 select-none dark:text-white')}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</span>
|
|
97
|
+
)
|
|
98
|
+
}
|