@maestro-js/components 1.0.0-alpha.2 → 1.0.0-alpha.21
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/commands/add.ts +16 -1
- package/dist/components.json +1 -1
- package/dist/index.js +14 -1
- package/package.json +5 -5
- package/registry.json +610 -802
- package/scripts/build.ts +11 -4
- package/src/components/alert-dialog.tsx +3 -3
- package/src/components/avatar.tsx +1 -1
- package/src/components/button-link.tsx +3 -0
- package/src/components/button.tsx +4 -0
- package/src/components/checkbox.tsx +7 -7
- package/src/components/chip.tsx +7 -7
- package/src/components/container.tsx +3 -3
- package/src/components/date-range.tsx +138 -0
- package/src/components/dialog.tsx +21 -7
- package/src/components/disclosure.tsx +1 -1
- package/src/components/drawer.tsx +19 -5
- package/src/components/form.tsx +27 -5
- package/src/components/helpers/form-field.tsx +7 -8
- package/src/components/helpers/get-button-classes.ts +23 -23
- package/src/components/helpers/headless-button.tsx +1 -1
- package/src/components/inline-alert.tsx +3 -3
- package/src/components/labeled-value.tsx +2 -2
- package/src/components/menu.tsx +60 -26
- package/src/components/month-day-input.tsx +2 -1
- package/src/components/multiselect.tsx +74 -70
- package/src/components/optional-link.tsx +15 -0
- package/src/components/radio.tsx +8 -8
- package/src/components/select.tsx +9 -8
- package/src/components/stepper.tsx +6 -6
- package/src/components/switch.tsx +7 -7
- package/src/components/table/headless-templated-row-table.ts +550 -0
- package/src/components/table/index.tsx +41 -0
- package/src/components/table/server-table.ts +143 -0
- package/src/components/table/table-actions.tsx +76 -0
- package/src/components/table/table-container.tsx +173 -0
- package/src/components/table/table-container.type-test.ts +155 -0
- package/src/components/table/table-filters/date-range-filter.tsx +227 -0
- package/src/components/table/table-filters/select-filter.tsx +211 -0
- package/src/components/table/table-filters/sort.tsx +85 -0
- package/src/components/table/table-primitives.tsx +226 -0
- package/src/components/table/table-toolbar.tsx +300 -0
- package/src/components/table/table-types.ts +61 -0
- package/src/components/table/use-client-table.ts +157 -0
- package/src/components/table/use-client-table.type-test.ts +217 -0
- package/src/components/table/use-server-table.ts +192 -0
- package/src/components/table/use-server-table.type-test.ts +174 -0
- package/src/components/tabs.tsx +1 -1
- package/src/components/tag-field.tsx +3 -3
- package/src/components/text-area.tsx +1 -1
- package/src/components/text-field.tsx +4 -1
- package/src/components/toast.tsx +6 -6
- package/src/components/toggle-button-group.tsx +2 -2
- package/src/utils/icons.d.ts +2 -1
- package/src/utils/use-query-state.ts +626 -0
- package/src/utils/use-tab-indicator.ts +9 -9
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { dateFns, type Iso } from 'iso-fns'
|
|
4
|
+
import { Button } from '../../button'
|
|
5
|
+
import type { StandardTable } from '../table-types'
|
|
6
|
+
import { Button as AriaButton, Dialog, DialogTrigger, Popover } from 'react-aria-components'
|
|
7
|
+
import { Icon } from '../../icon'
|
|
8
|
+
import { DateRangePicker, type DateRangeRootAdditionalProps } from '../../date-range'
|
|
9
|
+
|
|
10
|
+
export function TableDateRangeFilter({
|
|
11
|
+
table,
|
|
12
|
+
label,
|
|
13
|
+
value,
|
|
14
|
+
name,
|
|
15
|
+
options
|
|
16
|
+
}: {
|
|
17
|
+
table: StandardTable
|
|
18
|
+
label: string
|
|
19
|
+
name: string
|
|
20
|
+
value: null | { startDate: Iso.Date; endDate: Iso.Date }
|
|
21
|
+
options?: Partial<
|
|
22
|
+
Pick<DateRangeRootAdditionalProps, 'shouldDisableDate' | 'shouldDisableEndDate' | 'shouldDisableStartDate'>
|
|
23
|
+
> & {
|
|
24
|
+
quickPresets?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]
|
|
25
|
+
}
|
|
26
|
+
}) {
|
|
27
|
+
const [startDate, endDate] = value ? [value.startDate, value.endDate] : [null, null]
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<DialogTrigger>
|
|
31
|
+
<AriaButton
|
|
32
|
+
className={twMerge(
|
|
33
|
+
'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',
|
|
34
|
+
'rounded-full border data-[pressed]:scale-[.98]',
|
|
35
|
+
'md:my-3',
|
|
36
|
+
value ? 'text-primary-400 font-semibold' : ''
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
{label} {startDate && endDate ? getTableDateRangeFilterValue({ startDate, endDate }, options?.quickPresets) : ''}
|
|
40
|
+
<Icon name="chevron-down" className="ml-1 h-5 w-5" />
|
|
41
|
+
</AriaButton>
|
|
42
|
+
<Popover offset={4} className="z-20 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 bg-white">
|
|
43
|
+
<Dialog className="outline-none">
|
|
44
|
+
{({ close }) => (
|
|
45
|
+
<TableDateRangeFilterInner
|
|
46
|
+
table={table}
|
|
47
|
+
filterName={name}
|
|
48
|
+
closePopover={close}
|
|
49
|
+
defaultStart={startDate}
|
|
50
|
+
defaultEnd={endDate}
|
|
51
|
+
shouldDisableDate={options?.shouldDisableDate ?? (() => false)}
|
|
52
|
+
shouldDisableEndDate={options?.shouldDisableEndDate ?? (() => false)}
|
|
53
|
+
shouldDisableStartDate={options?.shouldDisableStartDate ?? (() => false)}
|
|
54
|
+
quickPresets={options?.quickPresets}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
</Dialog>
|
|
58
|
+
</Popover>
|
|
59
|
+
</DialogTrigger>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function TableDateRangeFilterInner({
|
|
64
|
+
table,
|
|
65
|
+
filterName,
|
|
66
|
+
closePopover,
|
|
67
|
+
defaultStart,
|
|
68
|
+
defaultEnd,
|
|
69
|
+
shouldDisableDate,
|
|
70
|
+
shouldDisableEndDate,
|
|
71
|
+
shouldDisableStartDate,
|
|
72
|
+
quickPresets
|
|
73
|
+
}: {
|
|
74
|
+
table: StandardTable
|
|
75
|
+
filterName: string
|
|
76
|
+
closePopover(): void
|
|
77
|
+
quickPresets?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]
|
|
78
|
+
} & DateRangeRootAdditionalProps) {
|
|
79
|
+
const [start, setStart] = React.useState<DateRangeRootAdditionalProps['defaultStart']>(defaultStart)
|
|
80
|
+
const [end, setEnd] = React.useState<DateRangeRootAdditionalProps['defaultEnd']>(defaultEnd)
|
|
81
|
+
const [currentField, setCurrentField] = React.useState<'start' | 'end' | null>(null)
|
|
82
|
+
|
|
83
|
+
const isPresetAvailable = !!quickPresets?.length
|
|
84
|
+
|
|
85
|
+
const applyFilter = (dateRange: { startDate: Iso.Date; endDate: Iso.Date }) => {
|
|
86
|
+
table.setFilters((prev: Record<string, any>) => ({ ...prev, [filterName]: dateRange }))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const onQuickPresetClick = (presetValue: { startDate: Iso.Date; endDate: Iso.Date }) => {
|
|
90
|
+
setCurrentField(null)
|
|
91
|
+
closePopover()
|
|
92
|
+
applyFilter(presetValue)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Note: DateRangePicker and its useDateRangePicker hook need to be adapted
|
|
96
|
+
// from headless-primitives. For now we pass through the same shape.
|
|
97
|
+
const pickerControls = {
|
|
98
|
+
start: start ?? null,
|
|
99
|
+
end: end ?? null,
|
|
100
|
+
onStartChange(v: Iso.Date | null, f: 'start' | 'end' | null) {
|
|
101
|
+
setStart(v)
|
|
102
|
+
setCurrentField(f)
|
|
103
|
+
},
|
|
104
|
+
onEndChange(v: Iso.Date | null, f: 'start' | 'end' | null) {
|
|
105
|
+
setEnd(v)
|
|
106
|
+
setCurrentField(f)
|
|
107
|
+
},
|
|
108
|
+
currentField,
|
|
109
|
+
shouldDisableDate,
|
|
110
|
+
shouldDisableEndDate,
|
|
111
|
+
shouldDisableStartDate
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="flex bg-white overflow-auto rounded-md text-base sm:text-sm">
|
|
116
|
+
{isPresetAvailable ? (
|
|
117
|
+
<QuickPresetPanel
|
|
118
|
+
start={start}
|
|
119
|
+
end={end}
|
|
120
|
+
containerClassName="border-solid border-r border-gray-200 py-2 overflow-y-auto sm:min-w-[200px] hidden sm:block"
|
|
121
|
+
className={twMerge('px-4 py-2 w-full text-left')}
|
|
122
|
+
quickPresets={quickPresets}
|
|
123
|
+
onQuickPresetClick={onQuickPresetClick}
|
|
124
|
+
/>
|
|
125
|
+
) : null}
|
|
126
|
+
<div>
|
|
127
|
+
<div className="px-3 py-3">
|
|
128
|
+
<DateRangePicker {...pickerControls} />
|
|
129
|
+
</div>
|
|
130
|
+
{isPresetAvailable ? (
|
|
131
|
+
<QuickPresetPanel
|
|
132
|
+
start={start}
|
|
133
|
+
end={end}
|
|
134
|
+
containerClassName="border-solid border-y border-gray-200 p-2 flex gap-1 flex-wrap sm:hidden"
|
|
135
|
+
className={twMerge('px-3 py-1 rounded-full border')}
|
|
136
|
+
quickPresets={quickPresets}
|
|
137
|
+
onQuickPresetClick={onQuickPresetClick}
|
|
138
|
+
/>
|
|
139
|
+
) : null}
|
|
140
|
+
<div className="p-2 float-right flex gap-2">
|
|
141
|
+
<Button variant="soft" onClick={closePopover}>
|
|
142
|
+
Cancel
|
|
143
|
+
</Button>
|
|
144
|
+
<Button
|
|
145
|
+
onClick={() => {
|
|
146
|
+
closePopover()
|
|
147
|
+
if (start && end && start <= end) {
|
|
148
|
+
applyFilter({ startDate: start as Iso.Date, endDate: end as Iso.Date })
|
|
149
|
+
}
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
Apply
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function QuickPresetPanel({
|
|
161
|
+
start,
|
|
162
|
+
end,
|
|
163
|
+
quickPresets,
|
|
164
|
+
onQuickPresetClick,
|
|
165
|
+
className,
|
|
166
|
+
containerClassName
|
|
167
|
+
}: {
|
|
168
|
+
start?: string | null
|
|
169
|
+
end?: string | null
|
|
170
|
+
quickPresets: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]
|
|
171
|
+
onQuickPresetClick: (value: { startDate: Iso.Date; endDate: Iso.Date }) => void
|
|
172
|
+
className?: string
|
|
173
|
+
containerClassName?: string
|
|
174
|
+
}) {
|
|
175
|
+
return (
|
|
176
|
+
<div className={containerClassName}>
|
|
177
|
+
{quickPresets.map(({ label, value }) => {
|
|
178
|
+
const [presetStartDate, presetEndDate] = [value.startDate, value.endDate]
|
|
179
|
+
return (
|
|
180
|
+
<button
|
|
181
|
+
key={label}
|
|
182
|
+
className={twMerge(
|
|
183
|
+
'text-sm cursor-pointer',
|
|
184
|
+
className,
|
|
185
|
+
'text-gray-900 bg-white text-nowrap',
|
|
186
|
+
'hover:text-white hover:bg-primary-500',
|
|
187
|
+
'focus:text-white focus:bg-primary-500',
|
|
188
|
+
presetStartDate === start && presetEndDate === end && 'bg-primary-500 text-white'
|
|
189
|
+
)}
|
|
190
|
+
onClick={() => onQuickPresetClick(value)}
|
|
191
|
+
>
|
|
192
|
+
{label}
|
|
193
|
+
</button>
|
|
194
|
+
)
|
|
195
|
+
})}
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getTableDateRangeFilterValue(
|
|
201
|
+
value: { startDate: Iso.Date; endDate: Iso.Date },
|
|
202
|
+
quickPreset?: { label: string; value: { startDate: Iso.Date; endDate: Iso.Date } }[]
|
|
203
|
+
) {
|
|
204
|
+
if (quickPreset) {
|
|
205
|
+
const preset = quickPreset.find((p) => p.value.startDate === value.startDate && p.value.endDate === value.endDate)
|
|
206
|
+
if (preset) {
|
|
207
|
+
return preset.label
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (dateFns.getYear(value.startDate) !== dateFns.getYear(value.endDate)) {
|
|
211
|
+
return getFormattedDateRange(value, 'MMM dd yyyy')
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
dateFns.getYear(value.startDate) === dateFns.getYear(dateFns.now()) &&
|
|
215
|
+
dateFns.getYear(value.endDate) === dateFns.getYear(dateFns.now())
|
|
216
|
+
) {
|
|
217
|
+
return getFormattedDateRange(value, 'MMM dd')
|
|
218
|
+
}
|
|
219
|
+
return getFormattedDateRange(value, 'MMM dd yyyy')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getFormattedDateRange(value: { startDate: Iso.Date; endDate: Iso.Date }, format: string) {
|
|
223
|
+
if (value.startDate === value.endDate) {
|
|
224
|
+
return dateFns.format(value.startDate, format)
|
|
225
|
+
}
|
|
226
|
+
return `${dateFns.format(value.startDate, format)} - ${dateFns.format(value.endDate, format)}`
|
|
227
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge'
|
|
2
|
+
import { Icon } from '../../icon'
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import type { StandardTable } from '../table-types'
|
|
5
|
+
import { Button, ComboBox, Input, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'
|
|
6
|
+
import React from 'react'
|
|
7
|
+
|
|
8
|
+
interface TableSelectFilterProps {
|
|
9
|
+
table: StandardTable
|
|
10
|
+
label: string
|
|
11
|
+
name: string
|
|
12
|
+
options: { label: string; value: any; className?: string; icon?: ReactNode }[]
|
|
13
|
+
value: any
|
|
14
|
+
autoComplete?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TableSelectFilter({ table, label, name, options, value, autoComplete }: TableSelectFilterProps) {
|
|
18
|
+
const setFilter = (newValue: any) => {
|
|
19
|
+
const toggledValue = newValue === value ? null : newValue
|
|
20
|
+
table.setFilters((prev: Record<string, any>) => ({ ...prev, [name]: toggledValue }))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (autoComplete) {
|
|
24
|
+
return <AutoCompleteFilter label={label} options={options} value={value} onSelect={setFilter} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return <DropdownFilter label={label} options={options} value={value} onSelect={setFilter} />
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function DropdownFilter({
|
|
31
|
+
label,
|
|
32
|
+
options,
|
|
33
|
+
value,
|
|
34
|
+
onSelect
|
|
35
|
+
}: {
|
|
36
|
+
label: string
|
|
37
|
+
options: TableSelectFilterProps['options']
|
|
38
|
+
value: any
|
|
39
|
+
onSelect: (value: any) => void
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<Select
|
|
43
|
+
aria-label={label}
|
|
44
|
+
selectedKey={value != null ? String(value) : null}
|
|
45
|
+
onSelectionChange={(key) => {
|
|
46
|
+
if (key === '__clear__') return onSelect(null)
|
|
47
|
+
const option = options.find((o) => String(o.value) === String(key))
|
|
48
|
+
onSelect(option?.value ?? null)
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<Button
|
|
52
|
+
className={twMerge(
|
|
53
|
+
'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',
|
|
54
|
+
'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',
|
|
55
|
+
'my-1',
|
|
56
|
+
value != null ? 'text-blue-600 font-medium' : ''
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{label}
|
|
60
|
+
<Icon name="chevron-down" className="ml-1 h-5 w-5" />
|
|
61
|
+
</Button>
|
|
62
|
+
<Popover offset={4} className="w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20">
|
|
63
|
+
<ListBox className="outline-none max-h-60 overflow-y-auto py-1">
|
|
64
|
+
{value != null && (
|
|
65
|
+
<ListBoxItem
|
|
66
|
+
id="__clear__"
|
|
67
|
+
textValue="Clear"
|
|
68
|
+
className={({ isFocused }) =>
|
|
69
|
+
twMerge(
|
|
70
|
+
'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',
|
|
71
|
+
isFocused ? 'text-white bg-primary-500' : 'text-gray-500'
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
Clear filter
|
|
76
|
+
</ListBoxItem>
|
|
77
|
+
)}
|
|
78
|
+
{options.map((o) => (
|
|
79
|
+
<FilterOption key={o.label} option={o} selected={value === o.value} />
|
|
80
|
+
))}
|
|
81
|
+
</ListBox>
|
|
82
|
+
</Popover>
|
|
83
|
+
</Select>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function FilterOption({
|
|
88
|
+
option,
|
|
89
|
+
selected
|
|
90
|
+
}: {
|
|
91
|
+
option: { label: string; value: any; className?: string; icon?: ReactNode }
|
|
92
|
+
selected: boolean
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<ListBoxItem
|
|
96
|
+
id={String(option.value)}
|
|
97
|
+
textValue={option.label}
|
|
98
|
+
className={({ isFocused }) =>
|
|
99
|
+
twMerge(
|
|
100
|
+
'group items-center text-sm px-4 py-2 relative flex justify-between w-full cursor-pointer outline-none',
|
|
101
|
+
isFocused ? 'text-white bg-primary-500' : 'text-gray-900',
|
|
102
|
+
option.className
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
>
|
|
106
|
+
{({ isFocused }) => (
|
|
107
|
+
<>
|
|
108
|
+
<div
|
|
109
|
+
className={twMerge(
|
|
110
|
+
selected ? 'font-semibold' : 'font-normal',
|
|
111
|
+
option.icon ? 'flex gap-1 items-center' : 'block'
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{option.icon}
|
|
115
|
+
{option.label}
|
|
116
|
+
</div>
|
|
117
|
+
{selected ? (
|
|
118
|
+
<div className={twMerge(isFocused ? 'text-white' : 'text-primary-500', 'flex items-center')}>
|
|
119
|
+
<Icon name="check" className="h-5 w-5" aria-hidden="true" />
|
|
120
|
+
</div>
|
|
121
|
+
) : null}
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</ListBoxItem>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface AutoCompleteFilterProps {
|
|
129
|
+
label: string
|
|
130
|
+
options: { label: string; value: any; className?: string; icon?: ReactNode }[]
|
|
131
|
+
value: any
|
|
132
|
+
onSelect: (value: any) => void
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function AutoCompleteFilter({ label, options, value, onSelect }: AutoCompleteFilterProps) {
|
|
136
|
+
const [query, setQuery] = React.useState('')
|
|
137
|
+
|
|
138
|
+
const filteredOptions =
|
|
139
|
+
query === ''
|
|
140
|
+
? options
|
|
141
|
+
: options.filter((o) => {
|
|
142
|
+
const q = query.toLowerCase()
|
|
143
|
+
return o.label.toLowerCase().includes(q) || String(o.value).toLowerCase().includes(q)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<ComboBox
|
|
148
|
+
aria-label={label}
|
|
149
|
+
inputValue={query}
|
|
150
|
+
onInputChange={setQuery}
|
|
151
|
+
selectedKey={value != null ? String(value) : null}
|
|
152
|
+
onSelectionChange={(key) => {
|
|
153
|
+
if (key === null) return
|
|
154
|
+
if (key === '__clear__') {
|
|
155
|
+
onSelect(null)
|
|
156
|
+
setQuery('')
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const option = options.find((o) => String(o.value) === String(key))
|
|
160
|
+
onSelect(option?.value ?? null)
|
|
161
|
+
setQuery('')
|
|
162
|
+
}}
|
|
163
|
+
menuTrigger="focus"
|
|
164
|
+
>
|
|
165
|
+
<div className="relative inline-block text-left">
|
|
166
|
+
<Button
|
|
167
|
+
className={twMerge(
|
|
168
|
+
'text-sm px-2 h-full flex text-gray-500 focus:outline-none whitespace-pre',
|
|
169
|
+
'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',
|
|
170
|
+
'my-1',
|
|
171
|
+
value != null ? 'text-blue-600 font-medium' : ''
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
{label}
|
|
175
|
+
<Icon name="chevron-down" className="ml-1 h-5 w-5" />
|
|
176
|
+
</Button>
|
|
177
|
+
</div>
|
|
178
|
+
<Popover offset={4} className="w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20">
|
|
179
|
+
<div className="flex p-2">
|
|
180
|
+
<Input
|
|
181
|
+
className={twMerge(
|
|
182
|
+
'text-sm block rounded-md focus:ring-0 border-0 focus:outline-none px-0 py-0 w-0 grow min-w-[30px]',
|
|
183
|
+
'py-1 px-1'
|
|
184
|
+
)}
|
|
185
|
+
placeholder="Filter..."
|
|
186
|
+
autoFocus
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
<ListBox className="outline-none max-h-60 overflow-y-auto py-1">
|
|
190
|
+
{value != null && (
|
|
191
|
+
<ListBoxItem
|
|
192
|
+
id="__clear__"
|
|
193
|
+
textValue="Clear"
|
|
194
|
+
className={({ isFocused }) =>
|
|
195
|
+
twMerge(
|
|
196
|
+
'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',
|
|
197
|
+
isFocused ? 'text-white bg-primary-500' : 'text-gray-500'
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
Clear filter
|
|
202
|
+
</ListBoxItem>
|
|
203
|
+
)}
|
|
204
|
+
{filteredOptions.map((o) => (
|
|
205
|
+
<FilterOption key={o.label} option={o} selected={value === o.value} />
|
|
206
|
+
))}
|
|
207
|
+
</ListBox>
|
|
208
|
+
</Popover>
|
|
209
|
+
</ComboBox>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { twMerge } from 'tailwind-merge'
|
|
2
|
+
import { Icon } from '../../icon'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import type { StandardTable } from '../table-types'
|
|
5
|
+
import { Button, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'
|
|
6
|
+
|
|
7
|
+
export interface TableSelectSortProps {
|
|
8
|
+
table: StandardTable
|
|
9
|
+
value: string | null
|
|
10
|
+
options: { value: string; label: string }[]
|
|
11
|
+
search: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TableSelectSort({ table, value, options, search }: TableSelectSortProps) {
|
|
15
|
+
const optionsWithRelevant = React.useMemo(() => {
|
|
16
|
+
return search ? [{ label: 'Most Relevant', value: 'mostRelevant' }, ...options] : options
|
|
17
|
+
}, [options, search])
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Select
|
|
21
|
+
aria-label="Sort"
|
|
22
|
+
selectedKey={value ?? null}
|
|
23
|
+
onSelectionChange={(key) => {
|
|
24
|
+
table.setSort(key === '__clear__' ? null : key ? String(key) : null)
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<Button
|
|
28
|
+
className={twMerge(
|
|
29
|
+
'text-sm px-2 h-full flex text-gray-500',
|
|
30
|
+
'rounded-full ring-1 ring-black/10 data-[pressed]:scale-[.98]',
|
|
31
|
+
'my-1'
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
Sort
|
|
35
|
+
<Icon name="chevron-down" className="ml-1 h-5 w-5" />
|
|
36
|
+
</Button>
|
|
37
|
+
<Popover offset={4} className="w-56 rounded-md shadow-lg ring-1 ring-black/10 bg-white z-20">
|
|
38
|
+
<ListBox className="outline-none max-h-60 overflow-y-auto py-1">
|
|
39
|
+
{value != null && (
|
|
40
|
+
<ListBoxItem
|
|
41
|
+
key="__clear__"
|
|
42
|
+
id="__clear__"
|
|
43
|
+
textValue="Clear"
|
|
44
|
+
className={({ isFocused }) =>
|
|
45
|
+
twMerge(
|
|
46
|
+
'text-sm px-4 py-2 cursor-pointer outline-none border-b border-gray-100',
|
|
47
|
+
isFocused ? 'text-white bg-primary-500' : 'text-gray-500'
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
>
|
|
51
|
+
Clear sort
|
|
52
|
+
</ListBoxItem>
|
|
53
|
+
)}
|
|
54
|
+
{optionsWithRelevant.map((o) => {
|
|
55
|
+
const selected = value === o.value
|
|
56
|
+
return (
|
|
57
|
+
<ListBoxItem
|
|
58
|
+
key={o.value}
|
|
59
|
+
id={o.value}
|
|
60
|
+
textValue={o.label}
|
|
61
|
+
className={({ isFocused }) =>
|
|
62
|
+
twMerge(
|
|
63
|
+
'group items-center text-sm px-4 py-2 relative flex justify-between w-full cursor-pointer outline-none',
|
|
64
|
+
isFocused ? 'text-white bg-primary-500' : 'text-gray-900'
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
{({ isFocused }) => (
|
|
69
|
+
<>
|
|
70
|
+
<span className={twMerge(selected ? 'font-semibold' : 'font-normal', 'block')}>{o.label}</span>
|
|
71
|
+
{selected ? (
|
|
72
|
+
<span className={twMerge(isFocused ? 'text-white' : 'text-primary-500', 'flex items-center')}>
|
|
73
|
+
<Icon name="check" className="h-5 w-5" aria-hidden="true" />
|
|
74
|
+
</span>
|
|
75
|
+
) : null}
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
</ListBoxItem>
|
|
79
|
+
)
|
|
80
|
+
})}
|
|
81
|
+
</ListBox>
|
|
82
|
+
</Popover>
|
|
83
|
+
</Select>
|
|
84
|
+
)
|
|
85
|
+
}
|