@usace/groundwork 1.0.1

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.
Files changed (77) hide show
  1. package/.eslintrc.cjs +18 -0
  2. package/.github/workflows/deploy-docs.yml +59 -0
  3. package/.github/workflows/publish.yml +19 -0
  4. package/README.md +5 -0
  5. package/index.html +13 -0
  6. package/lib/catalyst/alert.jsx +110 -0
  7. package/lib/catalyst/avatar.jsx +64 -0
  8. package/lib/catalyst/badge.jsx +72 -0
  9. package/lib/catalyst/checkbox.jsx +160 -0
  10. package/lib/catalyst/dialog.jsx +101 -0
  11. package/lib/catalyst/dropdown.jsx +202 -0
  12. package/lib/catalyst/fieldset.jsx +87 -0
  13. package/lib/catalyst/input.jsx +78 -0
  14. package/lib/catalyst/listbox.jsx +195 -0
  15. package/lib/catalyst/pagination.jsx +75 -0
  16. package/lib/catalyst/radio.jsx +144 -0
  17. package/lib/catalyst/select.jsx +78 -0
  18. package/lib/catalyst/switch.jsx +203 -0
  19. package/lib/catalyst/textarea.jsx +59 -0
  20. package/lib/components/breadcrumbs.jsx +46 -0
  21. package/lib/components/button.jsx +219 -0
  22. package/lib/components/display/headings.jsx +39 -0
  23. package/lib/components/display/text.jsx +48 -0
  24. package/lib/components/hero.jsx +51 -0
  25. package/lib/components/layout/container.jsx +13 -0
  26. package/lib/components/layout/usace-box.jsx +26 -0
  27. package/lib/components/link.jsx +18 -0
  28. package/lib/components/table.jsx +146 -0
  29. package/lib/composite/footer/essayons.jsx +12 -0
  30. package/lib/composite/footer/footer-links.jsx +33 -0
  31. package/lib/composite/footer/index.jsx +148 -0
  32. package/lib/composite/footer/logo-banner.jsx +55 -0
  33. package/lib/composite/footer/social-links.jsx +60 -0
  34. package/lib/composite/header/index.jsx +83 -0
  35. package/lib/composite/header/navbar-links.jsx +113 -0
  36. package/lib/composite/header/usa-banner/USABanner.jsx +114 -0
  37. package/lib/composite/header/usa-banner/usa-banner.css +220 -0
  38. package/lib/composite/site-wrapper/index.jsx +52 -0
  39. package/lib/index.jsx +19 -0
  40. package/package.json +39 -0
  41. package/postcss.config.js +6 -0
  42. package/public/Magnifying_glass_icon.svg +5 -0
  43. package/public/armystar-logo-rb.svg +4 -0
  44. package/public/cwbi-logo.png +0 -0
  45. package/public/essayonscrest.png +0 -0
  46. package/public/icon-dot-gov.svg +1 -0
  47. package/public/icon-https.svg +1 -0
  48. package/public/nww-lucky-peak-dam.jpg +0 -0
  49. package/public/rsgis-logo.png +0 -0
  50. package/public/us_flag_small.png +0 -0
  51. package/public/usace-logo-color.svg +1 -0
  52. package/src/App.jsx +38 -0
  53. package/src/app-bundles/index.js +4 -0
  54. package/src/app-bundles/route-bundle.js +36 -0
  55. package/src/app-components/code-example.jsx +17 -0
  56. package/src/app-components/copy-button.jsx +32 -0
  57. package/src/app-components/props-table.jsx +43 -0
  58. package/src/app-components/sub-site-banner.jsx +15 -0
  59. package/src/app-pages/documentation/_docs-page.jsx +31 -0
  60. package/src/app-pages/documentation/_docs-sidebar.jsx +111 -0
  61. package/src/app-pages/documentation/_template.jsx +55 -0
  62. package/src/app-pages/documentation/app-shell/index.jsx +39 -0
  63. package/src/app-pages/documentation/app-shell/site-wrapper.jsx +190 -0
  64. package/src/app-pages/documentation/buttons/index.jsx +170 -0
  65. package/src/app-pages/documentation/index.jsx +57 -0
  66. package/src/app-pages/documentation/layout/container.jsx +68 -0
  67. package/src/app-pages/documentation/layout/index.jsx +52 -0
  68. package/src/app-pages/documentation/layout/usace-box.jsx +76 -0
  69. package/src/app-pages/documentation/navigation/breadcrumbs.jsx +119 -0
  70. package/src/app-pages/documentation/navigation/index.jsx +31 -0
  71. package/src/app-pages/documentation/types/index.jsx +31 -0
  72. package/src/app-pages/documentation/types/link.jsx +72 -0
  73. package/src/app-pages/home.jsx +71 -0
  74. package/src/index.css +3 -0
  75. package/src/main.jsx +18 -0
  76. package/tailwind.config.js +31 -0
  77. package/vite.config.js +46 -0
@@ -0,0 +1,202 @@
1
+ "use client";
2
+
3
+ import {
4
+ Description as HeadlessDescription,
5
+ Label as HeadlessLabel,
6
+ Menu as HeadlessMenu,
7
+ MenuButton as HeadlessMenuButton,
8
+ MenuHeading as HeadlessMenuHeading,
9
+ MenuItem as HeadlessMenuItem,
10
+ MenuItems as HeadlessMenuItems,
11
+ MenuSection as HeadlessMenuSection,
12
+ MenuSeparator as HeadlessMenuSeparator,
13
+ Transition as HeadlessTransition,
14
+ } from "@headlessui/react";
15
+ import clsx from "clsx";
16
+ import { Fragment } from "react";
17
+ import { Button } from "../components/button";
18
+ import { Link } from "./link";
19
+
20
+ export function Dropdown(props) {
21
+ return <HeadlessMenu {...props} />;
22
+ }
23
+
24
+ export function DropdownButton(props) {
25
+ return <HeadlessMenuButton as={Button} {...props} />;
26
+ }
27
+
28
+ export function DropdownMenu({ anchor = "bottom", ...props }) {
29
+ return (
30
+ <HeadlessTransition
31
+ as={Fragment}
32
+ leave="duration-100 ease-in"
33
+ leaveTo="opacity-0"
34
+ >
35
+ <HeadlessMenuItems
36
+ {...props}
37
+ anchor={{
38
+ to: anchor,
39
+ gap: "var(--anchor-gap)",
40
+ offset: "var(--anchor-offset)",
41
+ padding: "var(--anchor-padding)",
42
+ }}
43
+ className={clsx(
44
+ props.className,
45
+
46
+ // Anchor positioning
47
+ "[--anchor-gap:theme(spacing.2)] [--anchor-padding:theme(spacing.3)] data-[anchor~=end]:[--anchor-offset:4px] data-[anchor~=start]:[--anchor-offset:-4px]",
48
+
49
+ // Base styles
50
+ "isolate w-max rounded-xl p-1",
51
+
52
+ // Invisible border that is only visible in `forced-colors` mode for accessibility purposes
53
+ "outline outline-1 outline-transparent focus:outline-none",
54
+
55
+ // Handle scrolling when menu won't fit in viewport
56
+ "overflow-y-auto",
57
+
58
+ // Popover background
59
+ "bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75",
60
+
61
+ // Shadows
62
+ "shadow-lg ring-1 ring-zinc-950/10 dark:ring-inset dark:ring-white/10",
63
+
64
+ // Define grid at the menu level if subgrid is supported
65
+ "supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]"
66
+ )}
67
+ />
68
+ </HeadlessTransition>
69
+ );
70
+ }
71
+
72
+ export function DropdownItem(props) {
73
+ return (
74
+ <HeadlessMenuItem
75
+ as={props.href ? Link : "button"}
76
+ type={props.href ? undefined : "button"}
77
+ {...props}
78
+ className={clsx(
79
+ props.className,
80
+
81
+ // Base styles
82
+ "group cursor-default rounded-lg px-3.5 py-2.5 focus:outline-none sm:px-3 sm:py-1.5",
83
+
84
+ // Text styles
85
+ "text-left text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]",
86
+
87
+ // Focus
88
+ "data-[focus]:bg-blue-500 data-[focus]:text-white",
89
+
90
+ // Disabled state
91
+ "data-[disabled]:opacity-50",
92
+
93
+ // Forced colors mode
94
+ "forced-color-adjust-none forced-colors:data-[focus]:bg-[Highlight] forced-colors:data-[focus]:text-[HighlightText] forced-colors:[&>[data-slot=icon]]:data-[focus]:text-[HighlightText]",
95
+
96
+ // Use subgrid when available but fallback to an explicit grid layout if not
97
+ "col-span-full grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] items-center supports-[grid-template-columns:subgrid]:grid-cols-subgrid",
98
+
99
+ // Icon
100
+ "[&>[data-slot=icon]]:col-start-1 [&>[data-slot=icon]]:row-start-1 [&>[data-slot=icon]]:mr-2.5 [&>[data-slot=icon]]:size-5 sm:[&>[data-slot=icon]]:mr-2 [&>[data-slot=icon]]:sm:size-4",
101
+ "[&>[data-slot=icon]]:text-zinc-500 [&>[data-slot=icon]]:data-[focus]:text-white [&>[data-slot=icon]]:dark:text-zinc-500 [&>[data-slot=icon]]:data-[focus]:dark:text-white"
102
+ )}
103
+ />
104
+ );
105
+ }
106
+
107
+ export function DropdownHeader({ className, ...props }) {
108
+ return (
109
+ <div
110
+ {...props}
111
+ className={clsx(className, "col-span-5 px-3.5 pb-1 pt-2.5 sm:px-3")}
112
+ />
113
+ );
114
+ }
115
+
116
+ export function DropdownSection({ className, ...props }) {
117
+ return (
118
+ <HeadlessMenuSection
119
+ {...props}
120
+ className={clsx(
121
+ className,
122
+ // Define grid at the section level instead of the item level if subgrid is supported
123
+ "col-span-full supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]"
124
+ )}
125
+ />
126
+ );
127
+ }
128
+
129
+ export function DropdownHeading({ className, ...props }) {
130
+ return (
131
+ <HeadlessMenuHeading
132
+ {...props}
133
+ className={clsx(
134
+ className,
135
+ "col-span-full grid grid-cols-[1fr,auto] gap-x-12 px-3.5 pb-1 pt-2 text-sm/5 font-medium text-zinc-500 sm:px-3 sm:text-xs/5 dark:text-zinc-400"
136
+ )}
137
+ />
138
+ );
139
+ }
140
+
141
+ export function DropdownSeparator({ className, ...props }) {
142
+ return (
143
+ <HeadlessMenuSeparator
144
+ {...props}
145
+ className={clsx(
146
+ className,
147
+ "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]"
148
+ )}
149
+ />
150
+ );
151
+ }
152
+
153
+ export function DropdownLabel({ className, ...props }) {
154
+ return (
155
+ <HeadlessLabel
156
+ {...props}
157
+ data-slot="label"
158
+ className={clsx(className, "col-start-2 row-start-1")}
159
+ {...props}
160
+ />
161
+ );
162
+ }
163
+
164
+ export function DropdownDescription({ className, ...props }) {
165
+ return (
166
+ <HeadlessDescription
167
+ data-slot="description"
168
+ {...props}
169
+ className={clsx(
170
+ className,
171
+ "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]"
172
+ )}
173
+ />
174
+ );
175
+ }
176
+
177
+ export function DropdownShortcut({ className, keys, ...props }) {
178
+ return (
179
+ <HeadlessDescription
180
+ as="kbd"
181
+ {...props}
182
+ className={clsx(
183
+ className,
184
+ "col-start-5 row-start-1 flex justify-self-end"
185
+ )}
186
+ >
187
+ {(Array.isArray(keys) ? keys : keys.split("")).map((char, index) => (
188
+ <kbd
189
+ key={index}
190
+ className={clsx([
191
+ "min-w-[2ch] text-center font-sans capitalize text-zinc-400 group-data-[focus]:text-white forced-colors:group-data-[focus]:text-[HighlightText]",
192
+
193
+ // Make sure key names that are longer than one character (like "Tab") have extra space
194
+ index > 0 && char.length > 1 && "pl-1",
195
+ ])}
196
+ >
197
+ {char}
198
+ </kbd>
199
+ ))}
200
+ </HeadlessDescription>
201
+ );
202
+ }
@@ -0,0 +1,87 @@
1
+ import {
2
+ Description as HeadlessDescription,
3
+ Field as HeadlessField,
4
+ Fieldset as HeadlessFieldset,
5
+ Label as HeadlessLabel,
6
+ Legend as HeadlessLegend,
7
+ } from '@headlessui/react'
8
+ import clsx from 'clsx'
9
+
10
+ export function Fieldset({ className, ...props }) {
11
+ return (
12
+ <HeadlessFieldset
13
+ {...props}
14
+ className={clsx(className, '[&>*+[data-slot=control]]:mt-6 [&>[data-slot=text]]:mt-1')}
15
+ />
16
+ )
17
+ }
18
+
19
+ export function Legend({ ...props }) {
20
+ return (
21
+ <HeadlessLegend
22
+ {...props}
23
+ data-slot="legend"
24
+ className={clsx(
25
+ props.className,
26
+ 'text-base/6 font-semibold text-zinc-950 data-[disabled]:opacity-50 sm:text-sm/6 dark:text-white'
27
+ )}
28
+ />
29
+ )
30
+ }
31
+
32
+ export function FieldGroup({ className, ...props }) {
33
+ return <div {...props} data-slot="control" className={clsx(className, 'space-y-8')} />
34
+ }
35
+
36
+ export function Field({ className, ...props }) {
37
+ return (
38
+ <HeadlessField
39
+ className={clsx(
40
+ className,
41
+ '[&>[data-slot=label]+[data-slot=control]]:mt-3',
42
+ '[&>[data-slot=label]+[data-slot=description]]:mt-1',
43
+ '[&>[data-slot=description]+[data-slot=control]]:mt-3',
44
+ '[&>[data-slot=control]+[data-slot=description]]:mt-3',
45
+ '[&>[data-slot=control]+[data-slot=error]]:mt-3',
46
+ '[&>[data-slot=label]]:font-medium'
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export function Label({ className, ...props }) {
54
+ return (
55
+ <HeadlessLabel
56
+ {...props}
57
+ data-slot="label"
58
+ className={clsx(
59
+ className,
60
+ 'select-none text-base/6 text-zinc-950 data-[disabled]:opacity-50 sm:text-sm/6 dark:text-white'
61
+ )}
62
+ />
63
+ )
64
+ }
65
+
66
+ export function Description({ className, disabled, ...props }) {
67
+ return (
68
+ <HeadlessDescription
69
+ {...props}
70
+ data-slot="description"
71
+ className={clsx(
72
+ className,
73
+ 'text-base/6 text-zinc-500 data-[disabled]:opacity-50 sm:text-sm/6 dark:text-zinc-400'
74
+ )}
75
+ />
76
+ )
77
+ }
78
+
79
+ export function ErrorMessage({ className, disabled, ...props }) {
80
+ return (
81
+ <HeadlessDescription
82
+ {...props}
83
+ data-slot="error"
84
+ className={clsx(className, 'text-base/6 text-red-600 data-[disabled]:opacity-50 sm:text-sm/6 dark:text-red-500')}
85
+ />
86
+ )
87
+ }
@@ -0,0 +1,78 @@
1
+ import { Input as HeadlessInput } from '@headlessui/react'
2
+ import { clsx } from 'clsx'
3
+ import { forwardRef } from 'react'
4
+
5
+ const dateTypes = ['date', 'datetime-local', 'month', 'time', 'week']
6
+
7
+ export const Input = forwardRef(function Input({ className, ...props }, ref) {
8
+ return (
9
+ <span
10
+ data-slot="control"
11
+ className={clsx([
12
+ className,
13
+
14
+ // Basic layout
15
+ 'relative block w-full',
16
+
17
+ // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
18
+ 'before:absolute before:inset-px before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-white before:shadow',
19
+
20
+ // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
21
+ 'dark:before:hidden',
22
+
23
+ // Focus ring
24
+ 'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-inset after:ring-transparent sm:after:focus-within:ring-2 sm:after:focus-within:ring-blue-500',
25
+
26
+ // Disabled state
27
+ 'has-[[data-disabled]]:opacity-50 before:has-[[data-disabled]]:bg-zinc-950/5 before:has-[[data-disabled]]:shadow-none',
28
+
29
+ // Invalid state
30
+ 'before:has-[[data-invalid]]:shadow-red-500/10',
31
+ ])}
32
+ >
33
+ <HeadlessInput
34
+ ref={ref}
35
+ className={clsx([
36
+ // Date classes
37
+ props.type &&
38
+ dateTypes.includes(props.type) && [
39
+ '[&::-webkit-datetime-edit-fields-wrapper]:p-0',
40
+ '[&::-webkit-date-and-time-value]:min-h-[1.5em]',
41
+ '[&::-webkit-datetime-edit]:inline-flex',
42
+ '[&::-webkit-datetime-edit]:p-0',
43
+ '[&::-webkit-datetime-edit-year-field]:p-0',
44
+ '[&::-webkit-datetime-edit-month-field]:p-0',
45
+ '[&::-webkit-datetime-edit-day-field]:p-0',
46
+ '[&::-webkit-datetime-edit-hour-field]:p-0',
47
+ '[&::-webkit-datetime-edit-minute-field]:p-0',
48
+ '[&::-webkit-datetime-edit-second-field]:p-0',
49
+ '[&::-webkit-datetime-edit-millisecond-field]:p-0',
50
+ '[&::-webkit-datetime-edit-meridiem-field]:p-0',
51
+ ],
52
+
53
+ // Basic layout
54
+ 'relative block w-full appearance-none rounded-lg px-[calc(theme(spacing[3.5])-1px)] py-[calc(theme(spacing[2.5])-1px)] sm:px-[calc(theme(spacing[3])-1px)] sm:py-[calc(theme(spacing[1.5])-1px)]',
55
+
56
+ // Typography
57
+ 'text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white',
58
+
59
+ // Border
60
+ 'border border-zinc-950/10 data-[hover]:border-zinc-950/20 dark:border-white/10 dark:data-[hover]:border-white/20',
61
+
62
+ // Background color
63
+ 'bg-transparent dark:bg-white/5',
64
+
65
+ // Hide default focus styles
66
+ 'focus:outline-none',
67
+
68
+ // Invalid state
69
+ 'data-[invalid]:border-red-500 data-[invalid]:data-[hover]:border-red-500 data-[invalid]:dark:border-red-500 data-[invalid]:data-[hover]:dark:border-red-500',
70
+
71
+ // Disabled state
72
+ 'data-[disabled]:border-zinc-950/20 dark:data-[hover]:data-[disabled]:border-white/15 data-[disabled]:dark:border-white/15 data-[disabled]:dark:bg-white/[2.5%]',
73
+ ])}
74
+ {...props}
75
+ />
76
+ </span>
77
+ )
78
+ })
@@ -0,0 +1,195 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Listbox as HeadlessListbox,
5
+ ListboxButton as HeadlessListboxButton,
6
+ ListboxOption as HeadlessListboxOption,
7
+ ListboxOptions as HeadlessListboxOptions,
8
+ ListboxSelectedOption as HeadlessListboxSelectedOption,
9
+ Transition as HeadlessTransition,
10
+ } from '@headlessui/react'
11
+ import clsx from 'clsx'
12
+ import { Fragment } from 'react'
13
+
14
+ export function Listbox({ className, placeholder, autoFocus, 'aria-label': ariaLabel, children: options, ...props }) {
15
+ return (
16
+ <HeadlessListbox {...props} multiple={false}>
17
+ <HeadlessListboxButton
18
+ autoFocus={autoFocus}
19
+ data-slot="control"
20
+ aria-label={ariaLabel}
21
+ className={clsx([
22
+ className,
23
+
24
+ // Basic layout
25
+ 'group relative block w-full',
26
+
27
+ // Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
28
+ 'before:absolute before:inset-px before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-white before:shadow',
29
+
30
+ // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
31
+ 'dark:before:hidden',
32
+
33
+ // Hide default focus styles
34
+ 'focus:outline-none',
35
+
36
+ // Focus ring
37
+ 'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-inset after:ring-transparent sm:after:data-[focus]:ring-2 sm:after:data-[focus]:ring-blue-500',
38
+
39
+ // Disabled state
40
+ 'data-[disabled]:opacity-50 before:data-[disabled]:bg-zinc-950/5 before:data-[disabled]:shadow-none',
41
+ ])}
42
+ >
43
+ <HeadlessListboxSelectedOption
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(theme(spacing[2.5])-1px)] sm:py-[calc(theme(spacing[1.5])-1px)]',
50
+
51
+ // Set minimum height for when no value is selected
52
+ 'min-h-11 sm:min-h-9',
53
+
54
+ // Horizontal padding
55
+ 'pl-[calc(theme(spacing[3.5])-1px)] pr-[calc(theme(spacing.7)-1px)] sm:pl-[calc(theme(spacing.3)-1px)]',
56
+
57
+ // Typography
58
+ 'text-left text-base/6 text-zinc-950 placeholder:text-zinc-500 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
59
+
60
+ // Border
61
+ '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',
62
+
63
+ // Background color
64
+ 'bg-transparent dark:bg-white/5',
65
+
66
+ // Invalid state
67
+ 'group-data-[invalid]:border-red-500 group-data-[invalid]:group-data-[hover]:border-red-500 group-data-[invalid]:dark:border-red-600 group-data-[invalid]:data-[hover]:dark:border-red-600',
68
+
69
+ // Disabled state
70
+ 'group-data-[disabled]:border-zinc-950/20 group-data-[disabled]:opacity-100 group-data-[disabled]:dark:border-white/15 group-data-[disabled]:dark:bg-white/[2.5%] dark:data-[hover]:group-data-[disabled]:border-white/15',
71
+ ])}
72
+ />
73
+ <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
74
+ <svg
75
+ className="size-5 stroke-zinc-500 group-data-[disabled]:stroke-zinc-600 sm:size-4 dark:stroke-zinc-400 forced-colors:stroke-[CanvasText]"
76
+ viewBox="0 0 16 16"
77
+ aria-hidden="true"
78
+ fill="none"
79
+ >
80
+ <path d="M5.75 10.75L8 13L10.25 10.75" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
81
+ <path d="M10.25 5.25L8 3L5.75 5.25" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
82
+ </svg>
83
+ </span>
84
+ </HeadlessListboxButton>
85
+ <HeadlessTransition
86
+ as={Fragment}
87
+ leave="transition-opacity duration-100 ease-in pointer-events-none"
88
+ leaveFrom="opacity-100"
89
+ leaveTo="opacity-0"
90
+ >
91
+ <HeadlessListboxOptions
92
+ as="div"
93
+ anchor={{
94
+ to: 'selection start',
95
+ offset: 'var(--anchor-offset)',
96
+ padding: 'var(--anchor-padding)',
97
+ }}
98
+ className={clsx(
99
+ // Anchor positioning
100
+ '[--anchor-offset:-1.625rem] [--anchor-padding:theme(spacing.4)] sm:[--anchor-offset:-1.375rem]',
101
+
102
+ // Base styles
103
+ 'isolate w-max min-w-[calc(var(--button-width)+1.75rem)] select-none scroll-py-1 rounded-xl p-1',
104
+
105
+ // Invisible border that is only visible in `forced-colors` mode for accessibility purposes
106
+ 'outline outline-1 outline-transparent focus:outline-none',
107
+
108
+ // Handle scrolling when menu won't fit in viewport
109
+ 'overflow-y-scroll overscroll-contain',
110
+
111
+ // Popover background
112
+ 'bg-white/75 backdrop-blur-xl dark:bg-zinc-800/75',
113
+
114
+ // Shadows
115
+ 'shadow-lg ring-1 ring-zinc-950/10 dark:ring-inset dark:ring-white/10'
116
+ )}
117
+ >
118
+ {options}
119
+ </HeadlessListboxOptions>
120
+ </HeadlessTransition>
121
+ </HeadlessListbox>
122
+ )
123
+ }
124
+
125
+ export function ListboxOption({ children, className, ...props }) {
126
+ const sharedClasses = clsx(
127
+ // Base
128
+ 'flex min-w-0 items-center',
129
+
130
+ // Icons
131
+ '[&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:shrink-0 [&>[data-slot=icon]]:text-zinc-500 [&>[data-slot=icon]]:group-data-[focus]/option:text-white sm:[&>[data-slot=icon]]:size-4 forced-colors:[&>[data-slot=icon]]:text-[CanvasText] forced-colors:[&>[data-slot=icon]]:group-data-[focus]/option:text-[Canvas]',
132
+
133
+ // Avatars
134
+ '[&>[data-slot=avatar]]:size-6 sm:[&>[data-slot=avatar]]:size-5'
135
+ )
136
+
137
+ return (
138
+ <HeadlessListboxOption as={Fragment} {...props}>
139
+ {({ selectedOption }) => {
140
+ if (selectedOption) {
141
+ return <div className={clsx(className, sharedClasses)}>{children}</div>
142
+ }
143
+
144
+ return (
145
+ <div
146
+ className={clsx(
147
+ // Basic layout
148
+ 'group/option grid cursor-default grid-cols-[theme(spacing.5),1fr] items-baseline gap-x-1.5 rounded-lg py-2.5 pl-2.5 pr-3.5 sm:grid-cols-[theme(spacing.4),1fr] sm:py-1.5 sm:pl-2 sm:pr-3',
149
+
150
+ // Typography
151
+ 'text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white forced-colors:text-[CanvasText]',
152
+
153
+ // Focus
154
+ 'outline-none data-[focus]:bg-blue-500 data-[focus]:text-white',
155
+
156
+ // Forced colors mode
157
+ 'forced-color-adjust-none forced-colors:data-[focus]:bg-[Highlight] forced-colors:data-[focus]:text-[HighlightText]',
158
+
159
+ // Disabled
160
+ 'data-[disabled]:opacity-50'
161
+ )}
162
+ >
163
+ <svg
164
+ className="relative hidden size-5 self-center stroke-current group-data-[selected]/option:inline sm:size-4"
165
+ viewBox="0 0 16 16"
166
+ fill="none"
167
+ aria-hidden="true"
168
+ >
169
+ <path d="M4 8.5l3 3L12 4" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
170
+ </svg>
171
+ <span className={clsx(className, sharedClasses, 'col-start-2')}>{children}</span>
172
+ </div>
173
+ )
174
+ }}
175
+ </HeadlessListboxOption>
176
+ )
177
+ }
178
+
179
+ export function ListboxLabel({ className, ...props }) {
180
+ return <span className={clsx(className, 'ml-2.5 truncate first:ml-0 sm:ml-2 sm:first:ml-0')} {...props} />
181
+ }
182
+
183
+ export function ListboxDescription({ className, children, ...props }) {
184
+ return (
185
+ <span
186
+ className={clsx(
187
+ className,
188
+ 'flex flex-1 overflow-hidden text-zinc-500 before:w-2 before:min-w-0 before:shrink group-data-[focus]/option:text-white dark:text-zinc-400'
189
+ )}
190
+ {...props}
191
+ >
192
+ <span className="flex-1 truncate">{children}</span>
193
+ </span>
194
+ )
195
+ }
@@ -0,0 +1,75 @@
1
+ import clsx from 'clsx'
2
+
3
+ import { Button } from './button'
4
+
5
+ export function Pagination({ 'aria-label': ariaLabel = 'Page navigation', className, ...props }) {
6
+ return <nav aria-label={ariaLabel} {...props} className={clsx(className, 'flex gap-x-2')} />
7
+ }
8
+
9
+ export function PaginationPrevious({ href = null, children = 'Previous' }) {
10
+ return (
11
+ <span className="grow basis-0">
12
+ <Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Previous page">
13
+ <svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
14
+ <path
15
+ d="M2.75 8H13.25M2.75 8L5.25 5.5M2.75 8L5.25 10.5"
16
+ strokeWidth={1.5}
17
+ strokeLinecap="round"
18
+ strokeLinejoin="round"
19
+ />
20
+ </svg>
21
+ {children}
22
+ </Button>
23
+ </span>
24
+ )
25
+ }
26
+
27
+ export function PaginationNext({ href = null, children = 'Next' }) {
28
+ return (
29
+ <span className="flex grow basis-0 justify-end">
30
+ <Button {...(href === null ? { disabled: true } : { href })} plain aria-label="Next page">
31
+ {children}
32
+ <svg className="stroke-current" data-slot="icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
33
+ <path
34
+ d="M13.25 8L2.75 8M13.25 8L10.75 10.5M13.25 8L10.75 5.5"
35
+ strokeWidth={1.5}
36
+ strokeLinecap="round"
37
+ strokeLinejoin="round"
38
+ />
39
+ </svg>
40
+ </Button>
41
+ </span>
42
+ )
43
+ }
44
+
45
+ export function PaginationList({ children }) {
46
+ return <span className="hidden items-baseline gap-x-2 sm:flex">{children}</span>
47
+ }
48
+
49
+ export function PaginationPage({ href, children, current = false }) {
50
+ return (
51
+ <Button
52
+ href={href}
53
+ plain
54
+ aria-label={`Page ${children}`}
55
+ aria-current={current ? 'page' : undefined}
56
+ className={clsx(
57
+ 'min-w-[2.25rem] before:absolute before:-inset-px before:rounded-lg',
58
+ current && 'before:bg-zinc-950/5 dark:before:bg-white/10'
59
+ )}
60
+ >
61
+ <span className="-mx-0.5">{children}</span>
62
+ </Button>
63
+ )
64
+ }
65
+
66
+ export function PaginationGap() {
67
+ return (
68
+ <div
69
+ aria-hidden="true"
70
+ className="w-[2.25rem] select-none text-center text-sm/6 font-semibold text-zinc-950 dark:text-white"
71
+ >
72
+ &hellip;
73
+ </div>
74
+ )
75
+ }