@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,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 = <>&hellip;</>,
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
+ }