@maestro-js/components 1.0.0-alpha.0
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 +41 -0
- package/commands/index.ts +7 -0
- package/commands/list.ts +9 -0
- package/dist/components.json +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +48 -0
- package/package.json +49 -0
- package/registry.json +1445 -0
- package/scripts/build.ts +44 -0
- package/src/components/alert-dialog.tsx +150 -0
- package/src/components/autocomplete.tsx +288 -0
- package/src/components/autosuggest.tsx +314 -0
- package/src/components/avatar.tsx +54 -0
- package/src/components/boolean-select.tsx +48 -0
- package/src/components/button-group.tsx +75 -0
- package/src/components/button-link.tsx +71 -0
- package/src/components/button.tsx +132 -0
- package/src/components/checkbox-group.tsx +172 -0
- package/src/components/checkbox.tsx +207 -0
- package/src/components/chip.tsx +158 -0
- package/src/components/container.tsx +82 -0
- package/src/components/currency-field.tsx +183 -0
- package/src/components/date-input.tsx +189 -0
- package/src/components/date-picker.tsx +211 -0
- package/src/components/date-range-picker.tsx +290 -0
- package/src/components/date-time-picker.tsx +196 -0
- package/src/components/dialog.tsx +97 -0
- package/src/components/disclosure.tsx +114 -0
- package/src/components/drawer.tsx +78 -0
- package/src/components/enum-chip.tsx +30 -0
- package/src/components/file-input.tsx +245 -0
- package/src/components/form.tsx +82 -0
- package/src/components/headless-file-input.tsx +362 -0
- package/src/components/helpers/animated-popover.tsx +24 -0
- package/src/components/helpers/button-context.ts +10 -0
- package/src/components/helpers/calendar-month-year-picker.tsx +280 -0
- package/src/components/helpers/form-field.tsx +229 -0
- package/src/components/helpers/get-button-classes.ts +138 -0
- package/src/components/helpers/headless-button.tsx +36 -0
- package/src/components/helpers/pdf-dist.client.ts +6 -0
- package/src/components/icon.tsx +26 -0
- package/src/components/image-input.tsx +265 -0
- package/src/components/img.tsx +46 -0
- package/src/components/inline-alert.tsx +54 -0
- package/src/components/labeled-value.tsx +480 -0
- package/src/components/link-tabs.tsx +118 -0
- package/src/components/menu.tsx +152 -0
- package/src/components/month-day-input.tsx +176 -0
- package/src/components/multi-file-input.tsx +244 -0
- package/src/components/multi-image-input.tsx +389 -0
- package/src/components/multicomplete.tsx +322 -0
- package/src/components/multiselect.tsx +325 -0
- package/src/components/multisuggest.tsx +357 -0
- package/src/components/number-field.tsx +143 -0
- package/src/components/numeric-tag-field.tsx +271 -0
- package/src/components/pdf-input.tsx +249 -0
- package/src/components/pdf.tsx +86 -0
- package/src/components/percentage-field.tsx +187 -0
- package/src/components/phone-number-field.tsx +166 -0
- package/src/components/radio-group.tsx +112 -0
- package/src/components/radio.tsx +91 -0
- package/src/components/select.tsx +215 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/stepper.tsx +181 -0
- package/src/components/switch.tsx +186 -0
- package/src/components/tabs.tsx +151 -0
- package/src/components/tag-field.tsx +250 -0
- package/src/components/text-area.tsx +148 -0
- package/src/components/text-field.tsx +144 -0
- package/src/components/time-input.tsx +198 -0
- package/src/components/toast.tsx +176 -0
- package/src/components/toggle-button-group.tsx +94 -0
- package/src/components/year-month-input.tsx +187 -0
- package/src/utils/colors.ts +44 -0
- package/src/utils/compose-refs.ts +32 -0
- package/src/utils/enum-colors.ts +1 -0
- package/src/utils/file-input.ts +49 -0
- package/src/utils/icons.d.ts +20 -0
- package/src/utils/tw.ts +13 -0
- package/src/utils/use-element-size.ts +35 -0
- package/src/utils/use-number-input.ts +143 -0
- package/src/utils/use-pagination.ts +38 -0
- package/src/utils/use-prevent-default.ts +27 -0
- package/src/utils/use-render-props.ts +39 -0
- package/src/utils/use-spin-delay.ts +24 -0
- package/src/utils/use-stable-accessor.ts +11 -0
- package/src/utils/use-tab-indicator.ts +106 -0
- package/tests/commands.test.ts +81 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { tw } from '../utils/tw'
|
|
3
|
+
import {
|
|
4
|
+
type Iso,
|
|
5
|
+
dateFns,
|
|
6
|
+
timeFns,
|
|
7
|
+
dateTimeFns,
|
|
8
|
+
instantFns,
|
|
9
|
+
zonedDateTimeFns,
|
|
10
|
+
yearMonthFns,
|
|
11
|
+
monthDayFns,
|
|
12
|
+
durationFns
|
|
13
|
+
} from 'iso-fns'
|
|
14
|
+
|
|
15
|
+
type LabeledValueBaseProps = {
|
|
16
|
+
label?: React.ReactNode
|
|
17
|
+
align?: 'start' | 'end'
|
|
18
|
+
labelAlign?: 'start' | 'end'
|
|
19
|
+
orientation?: 'horizontal' | 'vertical'
|
|
20
|
+
emptyText?: React.ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
labelClassName?: string
|
|
23
|
+
valueClassName?: string
|
|
24
|
+
id?: string
|
|
25
|
+
'aria-labelledby'?: string
|
|
26
|
+
'aria-describedby'?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type RangeValue<T> = { start: T; end: T }
|
|
30
|
+
|
|
31
|
+
type StringProps = {
|
|
32
|
+
value: string | null | undefined
|
|
33
|
+
formatOptions?: never
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type NumberProps = {
|
|
37
|
+
value: number | RangeValue<number> | null | undefined
|
|
38
|
+
formatOptions?: Intl.NumberFormatOptions
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type BooleanProps = {
|
|
42
|
+
value: boolean | null | undefined
|
|
43
|
+
formatOptions?: {
|
|
44
|
+
trueLabel?: string
|
|
45
|
+
falseLabel?: string
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type ReactNodeProps = {
|
|
50
|
+
value: React.ReactNode
|
|
51
|
+
formatOptions?: never
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type StringListProps = {
|
|
55
|
+
value: string[] | null | undefined
|
|
56
|
+
formatOptions?: Intl.ListFormatOptions
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type DateProps = {
|
|
60
|
+
value: Iso.Date | RangeValue<Iso.Date> | null | undefined
|
|
61
|
+
formatOptions?: {
|
|
62
|
+
dateFormat?: string
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type TimeProps = {
|
|
67
|
+
value: Iso.Time | RangeValue<Iso.Time> | null | undefined
|
|
68
|
+
formatOptions?: {
|
|
69
|
+
timeFormat?: string
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type DateTimeProps = {
|
|
74
|
+
value: Iso.DateTime | RangeValue<Iso.DateTime> | null | undefined
|
|
75
|
+
formatOptions?: {
|
|
76
|
+
dateTimeFormat?: string
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type InstantProps = {
|
|
81
|
+
value: Iso.Instant | RangeValue<Iso.Instant> | null | undefined
|
|
82
|
+
formatOptions?: {
|
|
83
|
+
dateTimeFormat?: string
|
|
84
|
+
timeZone?: string
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type ZonedDateTimeProps = {
|
|
89
|
+
value: Iso.ZonedDateTime | RangeValue<Iso.ZonedDateTime> | null | undefined
|
|
90
|
+
formatOptions?: {
|
|
91
|
+
dateTimeFormat?: string
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type YearMonthProps = {
|
|
96
|
+
value: Iso.YearMonth | RangeValue<Iso.YearMonth> | null | undefined
|
|
97
|
+
formatOptions?: {
|
|
98
|
+
dateFormat?: string
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type MonthDayProps = {
|
|
103
|
+
value: Iso.MonthDay | RangeValue<Iso.MonthDay> | null | undefined
|
|
104
|
+
formatOptions?: {
|
|
105
|
+
dateFormat?: string
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type DurationProps = {
|
|
110
|
+
value: Iso.Duration | RangeValue<Iso.Duration> | null | undefined
|
|
111
|
+
formatOptions?: {
|
|
112
|
+
style?: 'short' | 'long'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type LabeledValueFormatProps =
|
|
117
|
+
| StringProps
|
|
118
|
+
| NumberProps
|
|
119
|
+
| BooleanProps
|
|
120
|
+
| ReactNodeProps
|
|
121
|
+
| StringListProps
|
|
122
|
+
| DateProps
|
|
123
|
+
| TimeProps
|
|
124
|
+
| DateTimeProps
|
|
125
|
+
| InstantProps
|
|
126
|
+
| ZonedDateTimeProps
|
|
127
|
+
| YearMonthProps
|
|
128
|
+
| MonthDayProps
|
|
129
|
+
| DurationProps
|
|
130
|
+
|
|
131
|
+
export type LabeledValueProps = LabeledValueBaseProps & LabeledValueFormatProps
|
|
132
|
+
|
|
133
|
+
export const LabeledValue = React.forwardRef<HTMLDivElement, LabeledValueProps>(function LabeledValue(props, ref) {
|
|
134
|
+
const {
|
|
135
|
+
label,
|
|
136
|
+
align = 'start',
|
|
137
|
+
labelAlign = 'start',
|
|
138
|
+
orientation = 'vertical',
|
|
139
|
+
className,
|
|
140
|
+
labelClassName,
|
|
141
|
+
valueClassName,
|
|
142
|
+
id,
|
|
143
|
+
'aria-labelledby': ariaLabelledBy,
|
|
144
|
+
'aria-describedby': ariaDescribedBy
|
|
145
|
+
} = props
|
|
146
|
+
|
|
147
|
+
const labelId = React.useId()
|
|
148
|
+
const actualLabelId = id ?? labelId
|
|
149
|
+
|
|
150
|
+
const formattedValue = FormattedValue(props)
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
ref={ref}
|
|
155
|
+
className={tw('relative', orientation === 'horizontal' ? 'flex items-baseline gap-4' : 'block', className)}
|
|
156
|
+
>
|
|
157
|
+
{label && (
|
|
158
|
+
<LabeledValueLabel
|
|
159
|
+
id={actualLabelId}
|
|
160
|
+
className={tw(
|
|
161
|
+
orientation === 'horizontal' ? 'flex-shrink-0' : 'mb-1',
|
|
162
|
+
labelAlign === 'end' && 'text-right',
|
|
163
|
+
labelClassName
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
{label}
|
|
167
|
+
</LabeledValueLabel>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<div
|
|
171
|
+
className={tw(
|
|
172
|
+
'text-sm text-neutral-900 min-h-4 max-w-full break-words overflow-wrap-anywhere',
|
|
173
|
+
align === 'end' && 'text-right',
|
|
174
|
+
!formattedValue && 'text-neutral-400',
|
|
175
|
+
valueClassName
|
|
176
|
+
)}
|
|
177
|
+
aria-labelledby={ariaLabelledBy ?? (label ? actualLabelId : undefined)}
|
|
178
|
+
aria-describedby={ariaDescribedBy}
|
|
179
|
+
>
|
|
180
|
+
{formattedValue}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
LabeledValue.displayName = 'LabeledValue'
|
|
187
|
+
|
|
188
|
+
function FormattedValue(props: LabeledValueFormatProps & { emptyText?: React.ReactNode }) {
|
|
189
|
+
if (props.value == null || props.value === '') {
|
|
190
|
+
return <>{props.emptyText}</>
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (React.isValidElement(props.value)) {
|
|
194
|
+
return <>{props.value}</>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (typeof props.value === 'boolean') {
|
|
198
|
+
const boolProps = props as BooleanProps
|
|
199
|
+
return <>{props.value ? (boolProps.formatOptions?.trueLabel ?? 'Yes') : (boolProps.formatOptions?.falseLabel ?? 'No')}</>
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (Array.isArray(props.value)) {
|
|
203
|
+
return <ListValue {...(props as StringListProps)} />
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (typeof props.value === 'object' && props.value && 'start' in props.value && 'end' in props.value) {
|
|
207
|
+
if (typeof props.value.start === 'number') {
|
|
208
|
+
return <NumberValue {...(props as NumberProps)} />
|
|
209
|
+
}
|
|
210
|
+
if (typeof props.value.start === 'string' && dateFns.isValid(props.value.start)) {
|
|
211
|
+
return <DateValue {...(props as DateProps)} />
|
|
212
|
+
}
|
|
213
|
+
if (typeof props.value.start === 'string' && timeFns.isValid(props.value.start)) {
|
|
214
|
+
return <TimeValue {...(props as TimeProps)} />
|
|
215
|
+
}
|
|
216
|
+
if (typeof props.value.start === 'string' && dateTimeFns.isValid(props.value.start)) {
|
|
217
|
+
return <DateTimeValue {...(props as DateTimeProps)} />
|
|
218
|
+
}
|
|
219
|
+
if (typeof props.value.start === 'string' && instantFns.isValid(props.value.start)) {
|
|
220
|
+
return <InstantValue {...(props as InstantProps)} />
|
|
221
|
+
}
|
|
222
|
+
if (typeof props.value.start === 'string' && zonedDateTimeFns.isValid(props.value.start)) {
|
|
223
|
+
return <ZonedDateTimeValue {...(props as ZonedDateTimeProps)} />
|
|
224
|
+
}
|
|
225
|
+
if (typeof props.value.start === 'string' && yearMonthFns.isValid(props.value.start)) {
|
|
226
|
+
return <YearMonthValue {...(props as YearMonthProps)} />
|
|
227
|
+
}
|
|
228
|
+
if (typeof props.value.start === 'string' && monthDayFns.isValid(props.value.start)) {
|
|
229
|
+
return <MonthDayValue {...(props as MonthDayProps)} />
|
|
230
|
+
}
|
|
231
|
+
if (typeof props.value.start === 'string' && durationFns.isValid(props.value.start)) {
|
|
232
|
+
return <DurationValue {...(props as DurationProps)} />
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (typeof props.value === 'string') {
|
|
237
|
+
if (dateFns.isValid(props.value)) {
|
|
238
|
+
return <DateValue {...(props as DateProps)} />
|
|
239
|
+
}
|
|
240
|
+
if (timeFns.isValid(props.value)) {
|
|
241
|
+
return <TimeValue {...(props as TimeProps)} />
|
|
242
|
+
}
|
|
243
|
+
if (dateTimeFns.isValid(props.value)) {
|
|
244
|
+
return <DateTimeValue {...(props as DateTimeProps)} />
|
|
245
|
+
}
|
|
246
|
+
if (instantFns.isValid(props.value)) {
|
|
247
|
+
return <InstantValue {...(props as InstantProps)} />
|
|
248
|
+
}
|
|
249
|
+
if (zonedDateTimeFns.isValid(props.value)) {
|
|
250
|
+
return <ZonedDateTimeValue {...(props as ZonedDateTimeProps)} />
|
|
251
|
+
}
|
|
252
|
+
if (yearMonthFns.isValid(props.value)) {
|
|
253
|
+
return <YearMonthValue {...(props as YearMonthProps)} />
|
|
254
|
+
}
|
|
255
|
+
if (monthDayFns.isValid(props.value)) {
|
|
256
|
+
return <MonthDayValue {...(props as MonthDayProps)} />
|
|
257
|
+
}
|
|
258
|
+
if (durationFns.isValid(props.value)) {
|
|
259
|
+
return <DurationValue {...(props as DurationProps)} />
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return <>{props.value}</>
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (typeof props.value === 'number') {
|
|
266
|
+
return <NumberValue {...(props as NumberProps)} />
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return <>{String(props.value)}</>
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function NumberValue(props: NumberProps) {
|
|
273
|
+
const formatter = React.useMemo(() => new Intl.NumberFormat('en-us', props.formatOptions ?? {}), [props.formatOptions])
|
|
274
|
+
const formatted = React.useMemo(() => {
|
|
275
|
+
if (props.value && typeof props.value === 'object') {
|
|
276
|
+
return `${formatter.format(props.value.start)} – ${formatter.format(props.value.end)}`
|
|
277
|
+
} else if (typeof props.value === 'number') {
|
|
278
|
+
return formatter.format(props.value)
|
|
279
|
+
} else {
|
|
280
|
+
return props.value ?? null
|
|
281
|
+
}
|
|
282
|
+
}, [props.value, formatter])
|
|
283
|
+
return <>{formatted}</>
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function ListValue(props: StringListProps) {
|
|
287
|
+
const formatter = React.useMemo(
|
|
288
|
+
() => new Intl.ListFormat('en-us', props.formatOptions ?? { style: 'short' }),
|
|
289
|
+
[props.formatOptions]
|
|
290
|
+
)
|
|
291
|
+
const formatted = React.useMemo(() => {
|
|
292
|
+
if (props.value && props.value.length) {
|
|
293
|
+
return formatter.format(props.value)
|
|
294
|
+
} else {
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
}, [props.value, formatter])
|
|
298
|
+
return <>{formatted}</>
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function DateValue(props: DateProps) {
|
|
302
|
+
const format = props.formatOptions?.dateFormat ?? 'M/d/yyyy'
|
|
303
|
+
const formatted = React.useMemo(() => {
|
|
304
|
+
if (props.value && typeof props.value === 'object') {
|
|
305
|
+
return `${dateFns.format(props.value.start, format)} – ${dateFns.format(props.value.end, format)}`
|
|
306
|
+
} else if (typeof props.value === 'string') {
|
|
307
|
+
return dateFns.format(props.value, format)
|
|
308
|
+
} else {
|
|
309
|
+
return null
|
|
310
|
+
}
|
|
311
|
+
}, [props.value, format])
|
|
312
|
+
return <>{formatted}</>
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function TimeValue(props: TimeProps) {
|
|
316
|
+
const format = props.formatOptions?.timeFormat ?? 'h:mm a'
|
|
317
|
+
const formatted = React.useMemo(() => {
|
|
318
|
+
if (props.value && typeof props.value === 'object') {
|
|
319
|
+
return `${timeFns.format(props.value.start, format)} – ${timeFns.format(props.value.end, format)}`
|
|
320
|
+
} else if (typeof props.value === 'string') {
|
|
321
|
+
return timeFns.format(props.value, format)
|
|
322
|
+
} else {
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
}, [props.value, format])
|
|
326
|
+
return <>{formatted}</>
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function DateTimeValue(props: DateTimeProps) {
|
|
330
|
+
const format = props.formatOptions?.dateTimeFormat ?? 'M/d/yyyy h:mm a'
|
|
331
|
+
const formatted = React.useMemo(() => {
|
|
332
|
+
if (props.value && typeof props.value === 'object') {
|
|
333
|
+
return `${dateTimeFns.format(props.value.start, format)} – ${dateTimeFns.format(props.value.end, format)}`
|
|
334
|
+
} else if (typeof props.value === 'string') {
|
|
335
|
+
return dateTimeFns.format(props.value, format)
|
|
336
|
+
} else {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
}, [props.value, format])
|
|
340
|
+
return <>{formatted}</>
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function InstantValue(props: InstantProps) {
|
|
344
|
+
const timeZone = props.formatOptions?.timeZone
|
|
345
|
+
const format = props.formatOptions?.dateTimeFormat ?? (timeZone ? 'M/d/yyyy h:mm a z' : 'M/d/yyyy h:mm a')
|
|
346
|
+
|
|
347
|
+
const formatted = React.useMemo(() => {
|
|
348
|
+
const formatInstant = (instant: Iso.Instant) => {
|
|
349
|
+
if (timeZone) {
|
|
350
|
+
const zonedDateTime = instantFns.toZonedDateTime(instant, timeZone)
|
|
351
|
+
return zonedDateTimeFns.format(zonedDateTime, format)
|
|
352
|
+
}
|
|
353
|
+
return instantFns.chain(instant).toZonedDateTime('UTC').format(format).value()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (props.value && typeof props.value === 'object') {
|
|
357
|
+
return `${formatInstant(props.value.start)} – ${formatInstant(props.value.end)}`
|
|
358
|
+
} else if (typeof props.value === 'string') {
|
|
359
|
+
return formatInstant(props.value)
|
|
360
|
+
} else {
|
|
361
|
+
return null
|
|
362
|
+
}
|
|
363
|
+
}, [props.value, format, timeZone])
|
|
364
|
+
return <>{formatted}</>
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function ZonedDateTimeValue(props: ZonedDateTimeProps) {
|
|
368
|
+
const format = props.formatOptions?.dateTimeFormat ?? 'M/d/yyyy h:mm a z'
|
|
369
|
+
const formatted = React.useMemo(() => {
|
|
370
|
+
if (props.value && typeof props.value === 'object') {
|
|
371
|
+
return `${zonedDateTimeFns.format(props.value.start, format)} – ${zonedDateTimeFns.format(props.value.end, format)}`
|
|
372
|
+
} else if (typeof props.value === 'string') {
|
|
373
|
+
return zonedDateTimeFns.format(props.value, format)
|
|
374
|
+
} else {
|
|
375
|
+
return null
|
|
376
|
+
}
|
|
377
|
+
}, [props.value, format])
|
|
378
|
+
return <>{formatted}</>
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function YearMonthValue(props: YearMonthProps) {
|
|
382
|
+
const format = props.formatOptions?.dateFormat ?? 'MMMM yyyy'
|
|
383
|
+
const formatted = React.useMemo(() => {
|
|
384
|
+
if (props.value && typeof props.value === 'object') {
|
|
385
|
+
return `${yearMonthFns.format(props.value.start, format)} – ${yearMonthFns.format(props.value.end, format)}`
|
|
386
|
+
} else if (typeof props.value === 'string') {
|
|
387
|
+
return yearMonthFns.format(props.value, format)
|
|
388
|
+
} else {
|
|
389
|
+
return null
|
|
390
|
+
}
|
|
391
|
+
}, [props.value, format])
|
|
392
|
+
return <>{formatted}</>
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function MonthDayValue(props: MonthDayProps) {
|
|
396
|
+
const format = props.formatOptions?.dateFormat ?? 'MMMM d'
|
|
397
|
+
const formatted = React.useMemo(() => {
|
|
398
|
+
if (props.value && typeof props.value === 'object') {
|
|
399
|
+
return `${monthDayFns.format(props.value.start, format)} – ${monthDayFns.format(props.value.end, format)}`
|
|
400
|
+
} else if (typeof props.value === 'string') {
|
|
401
|
+
return monthDayFns.format(props.value, format)
|
|
402
|
+
} else {
|
|
403
|
+
return null
|
|
404
|
+
}
|
|
405
|
+
}, [props.value, format])
|
|
406
|
+
return <>{formatted}</>
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function DurationValue(props: DurationProps) {
|
|
410
|
+
const durationFormat = props.formatOptions?.style ?? 'short'
|
|
411
|
+
|
|
412
|
+
const formatted = React.useMemo(() => {
|
|
413
|
+
const formatDuration = (duration: Iso.Duration) => {
|
|
414
|
+
const fields = durationFns.getFields(duration)
|
|
415
|
+
|
|
416
|
+
const partFormatters: Record<string, Record<string, string>> = {
|
|
417
|
+
years: {
|
|
418
|
+
long: Math.abs(fields.years) > 1 ? `${fields.years} years` : `${fields.years} year`,
|
|
419
|
+
short: `${fields.years}y`
|
|
420
|
+
},
|
|
421
|
+
months: {
|
|
422
|
+
long: Math.abs(fields.months) > 1 ? `${fields.months} months` : `${fields.months} month`,
|
|
423
|
+
short: `${fields.months}M`
|
|
424
|
+
},
|
|
425
|
+
weeks: {
|
|
426
|
+
long: Math.abs(fields.weeks) > 1 ? `${fields.weeks} weeks` : `${fields.weeks} week`,
|
|
427
|
+
short: `${fields.weeks}w`
|
|
428
|
+
},
|
|
429
|
+
days: {
|
|
430
|
+
long: Math.abs(fields.days) > 1 ? `${fields.days} days` : `${fields.days} day`,
|
|
431
|
+
short: `${fields.days}d`
|
|
432
|
+
},
|
|
433
|
+
hours: {
|
|
434
|
+
long: Math.abs(fields.hours) > 1 ? `${fields.hours} hours` : `${fields.hours} hour`,
|
|
435
|
+
short: `${fields.hours}h`
|
|
436
|
+
},
|
|
437
|
+
minutes: {
|
|
438
|
+
long: Math.abs(fields.minutes) > 1 ? `${fields.minutes} minutes` : `${fields.minutes} minute`,
|
|
439
|
+
short: `${fields.minutes}m`
|
|
440
|
+
},
|
|
441
|
+
seconds: {
|
|
442
|
+
long: Math.abs(fields.seconds) > 1 ? `${fields.seconds} seconds` : `${fields.seconds} second`,
|
|
443
|
+
short: `${fields.seconds}s`
|
|
444
|
+
},
|
|
445
|
+
milliseconds: {
|
|
446
|
+
long:
|
|
447
|
+
Math.abs(fields.milliseconds) > 1 ? `${fields.milliseconds} milliseconds` : `${fields.milliseconds} millisecond`,
|
|
448
|
+
short: `${fields.milliseconds}ms`
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const parts = Object.entries(partFormatters)
|
|
453
|
+
.map(([key, value]) => (fields[key as keyof typeof fields] !== 0 ? value[durationFormat] : null))
|
|
454
|
+
.filter(Boolean) as string[]
|
|
455
|
+
if (!parts.length) {
|
|
456
|
+
return durationFormat === 'long' ? '0 seconds' : '0s'
|
|
457
|
+
} else {
|
|
458
|
+
return parts.join(' ')
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (props.value && typeof props.value === 'object') {
|
|
463
|
+
return `${formatDuration(props.value.start)} – ${formatDuration(props.value.end)}`
|
|
464
|
+
} else if (typeof props.value === 'string') {
|
|
465
|
+
return formatDuration(props.value)
|
|
466
|
+
} else {
|
|
467
|
+
return null
|
|
468
|
+
}
|
|
469
|
+
}, [props.value, durationFormat])
|
|
470
|
+
return <>{formatted}</>
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function LabeledValueLabel(props: React.ComponentProps<'label'>) {
|
|
474
|
+
return (
|
|
475
|
+
<label
|
|
476
|
+
{...props}
|
|
477
|
+
className={tw('block uppercase text-xs tracking-widest font-normal text-neutral-400', props.className)}
|
|
478
|
+
/>
|
|
479
|
+
)
|
|
480
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { NavLink } from 'react-router'
|
|
3
|
+
import type { To } from 'react-router'
|
|
4
|
+
import { tw } from '../utils/tw'
|
|
5
|
+
import {
|
|
6
|
+
TAB_BASE_CLASSES,
|
|
7
|
+
TAB_DISABLED_CLASSES,
|
|
8
|
+
TAB_INACTIVE_CLASSES,
|
|
9
|
+
TAB_OVAL_BASE_CLASSES,
|
|
10
|
+
TAB_OVAL_SELECTED_CLASSES,
|
|
11
|
+
tabColorMap,
|
|
12
|
+
tabListContainerClasses,
|
|
13
|
+
tabListInnerClasses,
|
|
14
|
+
getIndicatorClasses,
|
|
15
|
+
useAnimatedIndicator
|
|
16
|
+
} from '../utils/use-tab-indicator'
|
|
17
|
+
import type { TabColor, TabVariant } from '../utils/use-tab-indicator'
|
|
18
|
+
|
|
19
|
+
// --- Types ---
|
|
20
|
+
|
|
21
|
+
export type LinkTabListProps = {
|
|
22
|
+
children: React.ReactNode
|
|
23
|
+
className?: string
|
|
24
|
+
color?: TabColor
|
|
25
|
+
variant?: TabVariant
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type LinkTabProps = {
|
|
29
|
+
to: To
|
|
30
|
+
children: React.ReactNode
|
|
31
|
+
className?: string
|
|
32
|
+
disabled?: boolean
|
|
33
|
+
replace?: boolean
|
|
34
|
+
end?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- LinkTabList ---
|
|
38
|
+
|
|
39
|
+
const LinkTabListContext = React.createContext<{ color: TabColor; variant: TabVariant }>({ color: 'primary', variant: 'underline' })
|
|
40
|
+
|
|
41
|
+
function LinkTabList({ children, className, color = 'primary', variant = 'underline' }: LinkTabListProps) {
|
|
42
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
43
|
+
const indicatorRef = useAnimatedIndicator(containerRef, '[aria-current="page"]', 'aria-current')
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<LinkTabListContext.Provider value={{ color, variant }}>
|
|
47
|
+
<div ref={containerRef} className={tabListContainerClasses[variant]}>
|
|
48
|
+
<div role="tablist" className={tw(tabListInnerClasses[variant], className)}>
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
51
|
+
<div
|
|
52
|
+
ref={indicatorRef}
|
|
53
|
+
className={getIndicatorClasses(variant, color)}
|
|
54
|
+
style={{ opacity: 0 }}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</LinkTabListContext.Provider>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
LinkTabList.displayName = 'LinkTabList'
|
|
62
|
+
|
|
63
|
+
// --- LinkTab ---
|
|
64
|
+
|
|
65
|
+
function LinkTab({ to, children, className, disabled, replace, end }: LinkTabProps) {
|
|
66
|
+
const { color, variant } = React.useContext(LinkTabListContext)
|
|
67
|
+
const colors = tabColorMap[color]
|
|
68
|
+
const baseClasses = variant === 'oval' ? TAB_OVAL_BASE_CLASSES : TAB_BASE_CLASSES
|
|
69
|
+
const focusClasses = `outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:${colors.outline}`
|
|
70
|
+
|
|
71
|
+
if (disabled) {
|
|
72
|
+
return (
|
|
73
|
+
<span
|
|
74
|
+
role="tab"
|
|
75
|
+
aria-disabled="true"
|
|
76
|
+
className={tw(
|
|
77
|
+
baseClasses,
|
|
78
|
+
'outline-none',
|
|
79
|
+
TAB_INACTIVE_CLASSES,
|
|
80
|
+
TAB_DISABLED_CLASSES,
|
|
81
|
+
className
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</span>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<NavLink
|
|
91
|
+
to={to}
|
|
92
|
+
replace={replace}
|
|
93
|
+
end={end}
|
|
94
|
+
role="tab"
|
|
95
|
+
className={({ isActive }) =>
|
|
96
|
+
tw(
|
|
97
|
+
baseClasses,
|
|
98
|
+
focusClasses,
|
|
99
|
+
isActive
|
|
100
|
+
? (variant === 'oval' ? TAB_OVAL_SELECTED_CLASSES : `${colors.text} font-bold`)
|
|
101
|
+
: `${TAB_INACTIVE_CLASSES} hover:text-neutral-700`,
|
|
102
|
+
className
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</NavLink>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
LinkTab.displayName = 'LinkTab'
|
|
112
|
+
|
|
113
|
+
// --- Export ---
|
|
114
|
+
|
|
115
|
+
export const LinkTabs = {
|
|
116
|
+
List: LinkTabList,
|
|
117
|
+
Tab: LinkTab
|
|
118
|
+
}
|