@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.
Files changed (89) hide show
  1. package/commands/add.ts +41 -0
  2. package/commands/index.ts +7 -0
  3. package/commands/list.ts +9 -0
  4. package/dist/components.json +1 -0
  5. package/dist/index.d.ts +14 -0
  6. package/dist/index.js +48 -0
  7. package/package.json +49 -0
  8. package/registry.json +1445 -0
  9. package/scripts/build.ts +44 -0
  10. package/src/components/alert-dialog.tsx +150 -0
  11. package/src/components/autocomplete.tsx +288 -0
  12. package/src/components/autosuggest.tsx +314 -0
  13. package/src/components/avatar.tsx +54 -0
  14. package/src/components/boolean-select.tsx +48 -0
  15. package/src/components/button-group.tsx +75 -0
  16. package/src/components/button-link.tsx +71 -0
  17. package/src/components/button.tsx +132 -0
  18. package/src/components/checkbox-group.tsx +172 -0
  19. package/src/components/checkbox.tsx +207 -0
  20. package/src/components/chip.tsx +158 -0
  21. package/src/components/container.tsx +82 -0
  22. package/src/components/currency-field.tsx +183 -0
  23. package/src/components/date-input.tsx +189 -0
  24. package/src/components/date-picker.tsx +211 -0
  25. package/src/components/date-range-picker.tsx +290 -0
  26. package/src/components/date-time-picker.tsx +196 -0
  27. package/src/components/dialog.tsx +97 -0
  28. package/src/components/disclosure.tsx +114 -0
  29. package/src/components/drawer.tsx +78 -0
  30. package/src/components/enum-chip.tsx +30 -0
  31. package/src/components/file-input.tsx +245 -0
  32. package/src/components/form.tsx +82 -0
  33. package/src/components/headless-file-input.tsx +362 -0
  34. package/src/components/helpers/animated-popover.tsx +24 -0
  35. package/src/components/helpers/button-context.ts +10 -0
  36. package/src/components/helpers/calendar-month-year-picker.tsx +280 -0
  37. package/src/components/helpers/form-field.tsx +229 -0
  38. package/src/components/helpers/get-button-classes.ts +138 -0
  39. package/src/components/helpers/headless-button.tsx +36 -0
  40. package/src/components/helpers/pdf-dist.client.ts +6 -0
  41. package/src/components/icon.tsx +26 -0
  42. package/src/components/image-input.tsx +265 -0
  43. package/src/components/img.tsx +46 -0
  44. package/src/components/inline-alert.tsx +54 -0
  45. package/src/components/labeled-value.tsx +480 -0
  46. package/src/components/link-tabs.tsx +118 -0
  47. package/src/components/menu.tsx +152 -0
  48. package/src/components/month-day-input.tsx +176 -0
  49. package/src/components/multi-file-input.tsx +244 -0
  50. package/src/components/multi-image-input.tsx +389 -0
  51. package/src/components/multicomplete.tsx +322 -0
  52. package/src/components/multiselect.tsx +325 -0
  53. package/src/components/multisuggest.tsx +357 -0
  54. package/src/components/number-field.tsx +143 -0
  55. package/src/components/numeric-tag-field.tsx +271 -0
  56. package/src/components/pdf-input.tsx +249 -0
  57. package/src/components/pdf.tsx +86 -0
  58. package/src/components/percentage-field.tsx +187 -0
  59. package/src/components/phone-number-field.tsx +166 -0
  60. package/src/components/radio-group.tsx +112 -0
  61. package/src/components/radio.tsx +91 -0
  62. package/src/components/select.tsx +215 -0
  63. package/src/components/spinner.tsx +16 -0
  64. package/src/components/stepper.tsx +181 -0
  65. package/src/components/switch.tsx +186 -0
  66. package/src/components/tabs.tsx +151 -0
  67. package/src/components/tag-field.tsx +250 -0
  68. package/src/components/text-area.tsx +148 -0
  69. package/src/components/text-field.tsx +144 -0
  70. package/src/components/time-input.tsx +198 -0
  71. package/src/components/toast.tsx +176 -0
  72. package/src/components/toggle-button-group.tsx +94 -0
  73. package/src/components/year-month-input.tsx +187 -0
  74. package/src/utils/colors.ts +44 -0
  75. package/src/utils/compose-refs.ts +32 -0
  76. package/src/utils/enum-colors.ts +1 -0
  77. package/src/utils/file-input.ts +49 -0
  78. package/src/utils/icons.d.ts +20 -0
  79. package/src/utils/tw.ts +13 -0
  80. package/src/utils/use-element-size.ts +35 -0
  81. package/src/utils/use-number-input.ts +143 -0
  82. package/src/utils/use-pagination.ts +38 -0
  83. package/src/utils/use-prevent-default.ts +27 -0
  84. package/src/utils/use-render-props.ts +39 -0
  85. package/src/utils/use-spin-delay.ts +24 -0
  86. package/src/utils/use-stable-accessor.ts +11 -0
  87. package/src/utils/use-tab-indicator.ts +106 -0
  88. package/tests/commands.test.ts +81 -0
  89. package/tsconfig.json +27 -0
@@ -0,0 +1,183 @@
1
+ import * as React from 'react'
2
+ import { HeadlessForm } from '@maestro-js/form'
3
+ import type { ValidationFunction } from '@maestro-js/form'
4
+ import { FormFieldComponents } from './helpers/form-field'
5
+ import { useNumberInput } from '../utils/use-number-input'
6
+
7
+ export type CurrencyFieldProps = {
8
+ // Form identification
9
+ name?: string
10
+ form?: string
11
+
12
+ // Display elements
13
+ label?: React.ReactNode
14
+ cornerHint?: React.ReactNode
15
+ helpText?: React.ReactNode
16
+ placeholder?: string
17
+ warningMessage?: React.ReactNode
18
+
19
+ // Validation and state
20
+ isRequired?: boolean
21
+ isDisabled?: boolean
22
+ isReadonly?: boolean
23
+ isInvalid?: boolean
24
+ validate?: ValidationFunction<number>
25
+ errorMessage?: React.ReactNode | ((v: import('@maestro-js/form').ValidationResult) => React.ReactNode)
26
+
27
+ // Value management (in the unit specified by `units`)
28
+ value?: number | null
29
+ defaultValue?: number | null
30
+ onChange?: (value: number | null) => void
31
+
32
+ // Number specific
33
+ min?: number | null
34
+ max?: number | null
35
+ autoComplete?: string | null
36
+ autoFocus?: boolean
37
+
38
+ // Currency specific
39
+ units: 'dollars' | 'cents'
40
+ shape?: 'rectangle' | 'oval'
41
+ }
42
+
43
+ const currencyFormatOptions: Intl.NumberFormatOptions = {
44
+ style: 'currency',
45
+ currency: 'USD',
46
+ minimumFractionDigits: 2,
47
+ maximumFractionDigits: 2
48
+ }
49
+
50
+ export function CurrencyField(props: CurrencyFieldProps) {
51
+ const inputRef = React.useRef<HTMLInputElement>(null)
52
+ const hiddenInputRef = React.useRef<HTMLInputElement>(null)
53
+
54
+ const helpTextId = React.useId()
55
+ const inputId = React.useId()
56
+
57
+ const isCents = props.units === 'cents'
58
+
59
+ // Stable ref for onChange to avoid unnecessary re-renders in useNumberInput
60
+ const onChangeRef = React.useRef(props.onChange)
61
+ onChangeRef.current = props.onChange
62
+
63
+ const handleChange = React.useCallback(
64
+ (newValue: number | null) => {
65
+ onChangeRef.current?.(isCents && newValue !== null ? dollarsToCents(newValue) : newValue)
66
+ },
67
+ [isCents]
68
+ )
69
+
70
+ const displayValue = isCents && props.value != null ? centsToDollars(props.value) : props.value
71
+ const displayDefaultValue = isCents && props.defaultValue != null ? centsToDollars(props.defaultValue) : props.defaultValue
72
+ const displayMin = isCents && props.min != null ? centsToDollars(props.min) : props.min
73
+ const displayMax = isCents && props.max != null ? centsToDollars(props.max) : props.max
74
+
75
+ const numberInput = useNumberInput({
76
+ value: displayValue ?? undefined,
77
+ defaultValue: displayDefaultValue ?? undefined,
78
+ onChange: handleChange,
79
+ minValue: displayMin ?? undefined,
80
+ maxValue: displayMax ?? undefined,
81
+ step: 0.01,
82
+ formatOptions: currencyFormatOptions,
83
+ locale: 'en-US',
84
+ isDisabled: props.isDisabled,
85
+ isReadOnly: props.isReadonly ?? false
86
+ })
87
+
88
+ HeadlessForm.useReset(inputRef, numberInput.numberValue, numberInput.setNumberValue)
89
+
90
+ // Validation value should be in the original unit
91
+ const validationValue = isCents ? dollarsToCents(numberInput.numberValue) : numberInput.numberValue
92
+
93
+ const {
94
+ validationMessage,
95
+ isInvalid: validationInvalid,
96
+ commitValidation
97
+ } = HeadlessForm.useValidation(
98
+ {
99
+ validate: props.validate,
100
+ value: validationValue!,
101
+ name: props.name,
102
+ isRequired: props.isRequired,
103
+ form: props.form,
104
+ focus() {
105
+ inputRef.current?.focus()
106
+ }
107
+ },
108
+ hiddenInputRef
109
+ )
110
+
111
+ const isInvalid = props.isInvalid || validationInvalid
112
+ const hasWarning = Boolean(props.warningMessage) && !isInvalid
113
+
114
+ // Hidden input value in the original unit
115
+ const hiddenValue = isCents ? dollarsToCents(numberInput.numberValue) : numberInput.numberValue
116
+
117
+ const handleBlur = () => {
118
+ numberInput.commit(inputRef.current?.value)
119
+ commitValidation()
120
+ }
121
+
122
+ return (
123
+ <div className="relative">
124
+ <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} htmlFor={inputId} />
125
+
126
+ {/* Hidden input for form submission */}
127
+ {props.isDisabled ? null : (
128
+ <HeadlessForm.HiddenInput
129
+ ref={hiddenInputRef}
130
+ form={props.form}
131
+ name={props.name}
132
+ value={hiddenValue ?? ''}
133
+ type="number"
134
+ min={numberInput.minValue}
135
+ max={numberInput.maxValue}
136
+ />
137
+ )}
138
+
139
+ <FormFieldComponents.FieldGroup
140
+ isDisabled={props.isDisabled}
141
+ isReadonly={props.isReadonly}
142
+ isInvalid={isInvalid}
143
+ hasWarning={hasWarning}
144
+ shape={props.shape}
145
+ className="flex"
146
+ >
147
+ <FormFieldComponents.FieldInput
148
+ ref={inputRef}
149
+ id={inputId}
150
+ type="text"
151
+ value={numberInput.inputValue}
152
+ onChange={(e) => numberInput.setInputValue(e.currentTarget.value)}
153
+ onBlur={handleBlur}
154
+ placeholder={props.placeholder}
155
+ disabled={props.isDisabled}
156
+ readOnly={props.isReadonly}
157
+ autoComplete={props.autoComplete ?? undefined}
158
+ autoFocus={props.autoFocus}
159
+ aria-invalid={isInvalid || undefined}
160
+ aria-describedby={helpTextId}
161
+ />
162
+ </FormFieldComponents.FieldGroup>
163
+
164
+ <FormFieldComponents.FormFieldHelpText
165
+ id={helpTextId}
166
+ isInvalid={isInvalid}
167
+ validationMessage={validationMessage}
168
+ hasWarning={hasWarning}
169
+ warningMessage={props.warningMessage}
170
+ helpText={props.helpText}
171
+ />
172
+ </div>
173
+ )
174
+ }
175
+
176
+ function centsToDollars(cents: number): number {
177
+ return cents / 100
178
+ }
179
+
180
+ function dollarsToCents(dollars: number | null): number | null {
181
+ if (dollars == null) return null
182
+ return Math.round(dollars * 100)
183
+ }
@@ -0,0 +1,189 @@
1
+ import * as React from 'react'
2
+ import { DateField, DateInput as AriaDateInput, DateSegment, I18nProvider } from 'react-aria-components'
3
+ import { CalendarDate, parseDate } from '@internationalized/date'
4
+ import { tw } from '../utils/tw'
5
+ import { FormFieldComponents } from './helpers/form-field'
6
+ import { HeadlessForm } from '@maestro-js/form'
7
+ import type { ValidationFunction, ValidationResult } from '@maestro-js/form'
8
+ import { dateFns, type Iso } from 'iso-fns'
9
+
10
+ export type DateInputProps = {
11
+ // Form identification
12
+ name?: string
13
+ form?: string
14
+
15
+ // Display elements
16
+ label?: React.ReactNode
17
+ cornerHint?: React.ReactNode
18
+ helpText?: React.ReactNode
19
+ placeholder?: string
20
+ warningMessage?: React.ReactNode
21
+
22
+ // Validation and state
23
+ isRequired?: boolean
24
+ isDisabled?: boolean
25
+ isReadonly?: boolean
26
+ isInvalid?: boolean
27
+ validate?: ValidationFunction<Iso.Date | null>
28
+ errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)
29
+
30
+ // Value management (ISO date string YYYY-MM-DD)
31
+ value?: Iso.Date | null
32
+ defaultValue?: Iso.Date | null
33
+ onChange?: (value: Iso.Date | null) => void
34
+
35
+ // Date specific (ISO date strings)
36
+ minValue?: Iso.Date
37
+ maxValue?: Iso.Date
38
+ isDateUnavailable?: (date: Iso.Date) => boolean
39
+
40
+ // Locale
41
+ locale?: string
42
+ shape?: 'rectangle' | 'oval'
43
+ }
44
+
45
+ export function DateInput(props: DateInputProps) {
46
+ const hiddenInputRef = React.useRef<HTMLInputElement>(null)
47
+ const helpTextId = React.useId()
48
+ const labelId = React.useId()
49
+
50
+ // Internal state for CalendarDate
51
+ const [value, _setValue] = React.useState(() => {
52
+ try {
53
+ const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value
54
+ return startingDate ? parseDate(startingDate) : null
55
+ } catch {
56
+ return null
57
+ }
58
+ })
59
+
60
+ function setValue(d: CalendarDate | null) {
61
+ _setValue(d)
62
+ if (!d || dateFns.isValid(d.toString())) {
63
+ props.onChange && props.onChange(d ? (d.toString() as Iso.Date) : null)
64
+ }
65
+ commitValidation()
66
+ }
67
+
68
+ // Sync controlled value
69
+ if (props.value !== undefined) {
70
+ const currentValueAsIso = value && dateFns.isValid(value.toString()) ? value.toString() : null
71
+ const propValue = props.value && dateFns.isValid(props.value) ? props.value : null
72
+ if ((!value || dateFns.isValid(value.toString())) && propValue !== currentValueAsIso) {
73
+ try {
74
+ _setValue(propValue ? parseDate(propValue) : null)
75
+ } catch {
76
+ _setValue(null)
77
+ }
78
+ }
79
+ }
80
+
81
+ // Convert min/max dates
82
+ const minDate = React.useMemo(() => {
83
+ if (!props.minValue) return undefined
84
+ try {
85
+ return parseDate(props.minValue)
86
+ } catch {
87
+ return undefined
88
+ }
89
+ }, [props.minValue])
90
+
91
+ const maxDate = React.useMemo(() => {
92
+ if (!props.maxValue) return undefined
93
+ try {
94
+ return parseDate(props.maxValue)
95
+ } catch {
96
+ return undefined
97
+ }
98
+ }, [props.maxValue])
99
+
100
+ // Form validation
101
+ const {
102
+ validationMessage: validationMessageRaw,
103
+ isInvalid: validationInvalid,
104
+ commitValidation
105
+ } = HeadlessForm.useValidation(
106
+ {
107
+ validate: props.validate,
108
+ value: value && dateFns.isValid(value.toString()) ? (value.toString() as Iso.Date) : null,
109
+ name: props.name,
110
+ isRequired: props.isRequired,
111
+ form: props.form,
112
+ focus() {
113
+ // Focus handled by DateField
114
+ }
115
+ },
116
+ hiddenInputRef as React.RefObject<HTMLInputElement>
117
+ )
118
+
119
+ const lessThanMin =
120
+ props.minValue && value && dateFns.isValid(value.toString()) ? value.toString() < props.minValue : false
121
+ const greaterThanMax =
122
+ props.maxValue && value && dateFns.isValid(value.toString()) ? value.toString() > props.maxValue : false
123
+
124
+ const validationMessage =
125
+ validationMessageRaw ||
126
+ (lessThanMin ? `Cannot be less than ${dateFns.format(props.minValue!, 'MM/dd/yyyy')}` : '') ||
127
+ (greaterThanMax ? `Cannot be greater than ${dateFns.format(props.maxValue!, 'MM/dd/yyyy')}` : '')
128
+
129
+ const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax
130
+ const hasWarning = Boolean(props.warningMessage) && !isInvalid
131
+
132
+ return (
133
+ <I18nProvider locale={props.locale ?? 'en'}>
134
+ <div className="relative">
135
+ {/* Hidden input for form submission */}
136
+ <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />
137
+
138
+ <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />
139
+
140
+ {/* Date input */}
141
+ <DateField
142
+ aria-labelledby={labelId}
143
+ aria-describedby={helpTextId}
144
+ value={value}
145
+ onChange={setValue}
146
+ minValue={minDate}
147
+ maxValue={maxDate}
148
+ isDisabled={props.isDisabled}
149
+ isReadOnly={props.isReadonly}
150
+ isInvalid={isInvalid}
151
+ isRequired={props.isRequired}
152
+ isDateUnavailable={
153
+ props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined
154
+ }
155
+ >
156
+ <FormFieldComponents.FieldGroup
157
+ isDisabled={props.isDisabled}
158
+ isReadonly={props.isReadonly}
159
+ isInvalid={isInvalid}
160
+ hasWarning={hasWarning}
161
+ shape={props.shape}
162
+ >
163
+ <AriaDateInput className="flex px-3 py-2 text-sm">
164
+ {(segment) => (
165
+ <DateSegment
166
+ segment={segment}
167
+ className={tw(
168
+ 'px-0.5 tabular-nums outline-none rounded-sm',
169
+ 'focus:bg-primary-500 focus:text-white',
170
+ segment.isPlaceholder && 'text-neutral-400'
171
+ )}
172
+ />
173
+ )}
174
+ </AriaDateInput>
175
+ </FormFieldComponents.FieldGroup>
176
+ </DateField>
177
+
178
+ <FormFieldComponents.FormFieldHelpText
179
+ id={helpTextId}
180
+ isInvalid={isInvalid}
181
+ validationMessage={validationMessage}
182
+ hasWarning={hasWarning}
183
+ warningMessage={props.warningMessage}
184
+ helpText={props.helpText}
185
+ />
186
+ </div>
187
+ </I18nProvider>
188
+ )
189
+ }
@@ -0,0 +1,211 @@
1
+ import * as React from 'react'
2
+ import { DatePicker as AriaDatePicker, DateInput, DateSegment, Button, Dialog, I18nProvider } from 'react-aria-components'
3
+ import { AnimatedPopover } from './helpers/animated-popover'
4
+ import { CalendarDate, parseDate } from '@internationalized/date'
5
+ import { tw } from '../utils/tw'
6
+ import { FormFieldComponents } from './helpers/form-field'
7
+ import { Icon } from './icon'
8
+ import { HeadlessForm } from '@maestro-js/form'
9
+ import type { ValidationFunction, ValidationResult } from '@maestro-js/form'
10
+ import { CustomCalendar } from './helpers/calendar-month-year-picker'
11
+ import { dateFns, type Iso } from 'iso-fns'
12
+
13
+ export type DatePickerProps = {
14
+ // Form identification
15
+ name?: string
16
+ form?: string
17
+
18
+ // Display elements
19
+ label?: React.ReactNode
20
+ cornerHint?: React.ReactNode
21
+ helpText?: React.ReactNode
22
+ placeholder?: string
23
+ warningMessage?: React.ReactNode
24
+
25
+ // Validation and state
26
+ isRequired?: boolean
27
+ isDisabled?: boolean
28
+ isReadonly?: boolean
29
+ isInvalid?: boolean
30
+ validate?: ValidationFunction<Iso.Date | null>
31
+ errorMessage?: React.ReactNode | ((v: ValidationResult) => React.ReactNode)
32
+
33
+ // Value management (ISO date string YYYY-MM-DD)
34
+ value?: Iso.Date | null
35
+ defaultValue?: Iso.Date | null
36
+ onChange?: (value: Iso.Date | null) => void
37
+
38
+ // Date specific (ISO date strings)
39
+ minValue?: Iso.Date
40
+ maxValue?: Iso.Date
41
+ isDateUnavailable?: (date: Iso.Date) => boolean
42
+
43
+ // Calendar display
44
+ locale?: string
45
+
46
+ // Input shape
47
+ shape?: 'rectangle' | 'oval'
48
+ }
49
+
50
+ function DatePicker_(props: DatePickerProps) {
51
+ const hiddenInputRef = React.useRef<HTMLInputElement>(null)
52
+ const helpTextId = React.useId()
53
+ const labelId = React.useId()
54
+
55
+ // Internal state for CalendarDate
56
+ const [value, _setValue] = React.useState(() => {
57
+ try {
58
+ const startingDate = props.value === undefined ? (props.defaultValue ?? null) : props.value
59
+ return startingDate ? parseDate(startingDate) : null
60
+ } catch {
61
+ return null
62
+ }
63
+ })
64
+
65
+ function setValue(d: CalendarDate | null) {
66
+ _setValue(d)
67
+ if (!d || dateFns.isValid(d.toString())) {
68
+ props.onChange && props.onChange(d ? (d.toString() as Iso.Date) : null)
69
+ }
70
+ commitValidation()
71
+ }
72
+
73
+ // Sync controlled value
74
+ if (props.value !== undefined) {
75
+ const currentValueAsIso = value && dateFns.isValid(value.toString()) ? value.toString() : null
76
+ const propValue = props.value && dateFns.isValid(props.value) ? props.value : null
77
+ if ((!value || dateFns.isValid(value.toString())) && propValue !== currentValueAsIso) {
78
+ try {
79
+ _setValue(propValue ? parseDate(propValue) : null)
80
+ } catch {
81
+ _setValue(null)
82
+ }
83
+ }
84
+ }
85
+
86
+ // Convert min/max dates
87
+ const minDate = React.useMemo(() => {
88
+ if (!props.minValue) return undefined
89
+ try {
90
+ return parseDate(props.minValue)
91
+ } catch {
92
+ return undefined
93
+ }
94
+ }, [props.minValue])
95
+
96
+ const maxDate = React.useMemo(() => {
97
+ if (!props.maxValue) return undefined
98
+ try {
99
+ return parseDate(props.maxValue)
100
+ } catch {
101
+ return undefined
102
+ }
103
+ }, [props.maxValue])
104
+
105
+ // Form validation
106
+ const {
107
+ validationMessage: validationMessageRaw,
108
+ isInvalid: validationInvalid,
109
+ commitValidation
110
+ } = HeadlessForm.useValidation(
111
+ {
112
+ validate: props.validate,
113
+ value: value && dateFns.isValid(value.toString()) ? (value.toString() as Iso.Date) : null,
114
+ name: props.name,
115
+ isRequired: props.isRequired,
116
+ form: props.form,
117
+ focus() {
118
+ // Focus handled by AriaDatePicker
119
+ }
120
+ },
121
+ hiddenInputRef as React.RefObject<HTMLInputElement>
122
+ )
123
+
124
+ const lessThanMin =
125
+ props.minValue && value && dateFns.isValid(value.toString()) ? value.toString() < props.minValue : false
126
+ const greaterThanMax =
127
+ props.maxValue && value && dateFns.isValid(value.toString()) ? value.toString() > props.maxValue : false
128
+
129
+ const validationMessage =
130
+ validationMessageRaw ||
131
+ (lessThanMin ? `Cannot be less than ${dateFns.format(props.minValue!, 'MM/dd/yyyy')}` : '') ||
132
+ (greaterThanMax ? `Cannot be greater than ${dateFns.format(props.maxValue!, 'MM/dd/yyyy')}` : '')
133
+
134
+ const isInvalid = props.isInvalid || validationInvalid || lessThanMin || greaterThanMax
135
+ const hasWarning = Boolean(props.warningMessage) && !isInvalid
136
+
137
+ return (
138
+ <I18nProvider locale={props.locale ?? 'en'}>
139
+ <div className="relative">
140
+ {/* Hidden input for form submission */}
141
+ <HeadlessForm.HiddenInput ref={hiddenInputRef} name={props.name} value={value?.toString() ?? ''} form={props.form} />
142
+
143
+ <FormFieldComponents.LabelRow label={props.label} cornerHint={props.cornerHint} labelId={labelId} />
144
+
145
+ <AriaDatePicker
146
+ value={value}
147
+ aria-labelledby={labelId}
148
+ onChange={setValue}
149
+ minValue={minDate}
150
+ maxValue={maxDate}
151
+ isDateUnavailable={
152
+ props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined
153
+ }
154
+ isDisabled={props.isDisabled}
155
+ isReadOnly={props.isReadonly}
156
+ isInvalid={isInvalid}
157
+ isRequired={props.isRequired}
158
+ >
159
+ <FormFieldComponents.FieldGroup
160
+ isDisabled={props.isDisabled}
161
+ isReadonly={props.isReadonly}
162
+ isInvalid={isInvalid}
163
+ hasWarning={hasWarning}
164
+ shape={props.shape}
165
+ className="flex"
166
+ >
167
+ <DateInput className="flex-1 grow flex px-3 py-2 text-sm">
168
+ {(segment) => (
169
+ <DateSegment
170
+ segment={segment}
171
+ className={tw('px-0.5 tabular-nums outline-none rounded-sm', 'focus:bg-primary-500 focus:text-white')}
172
+ />
173
+ )}
174
+ </DateInput>
175
+ <Button className="flex items-center px-2 py-2">
176
+ <Icon name="calendar" className="h-4 w-4 text-neutral-400" />
177
+ </Button>
178
+ </FormFieldComponents.FieldGroup>
179
+
180
+ <AnimatedPopover>
181
+ <Dialog>
182
+ <CustomCalendar
183
+ value={value}
184
+ onChange={setValue}
185
+ minValue={minDate}
186
+ maxValue={maxDate}
187
+ isDateUnavailable={
188
+ props.isDateUnavailable ? (date) => props.isDateUnavailable!(date.toString() as Iso.Date) : undefined
189
+ }
190
+ />
191
+ </Dialog>
192
+ </AnimatedPopover>
193
+ </AriaDatePicker>
194
+
195
+ <FormFieldComponents.FormFieldHelpText
196
+ id={helpTextId}
197
+ isInvalid={isInvalid}
198
+ validationMessage={validationMessage}
199
+ hasWarning={hasWarning}
200
+ warningMessage={props.warningMessage}
201
+ helpText={props.helpText}
202
+ />
203
+ </div>
204
+ </I18nProvider>
205
+ )
206
+ }
207
+
208
+ const today = dateFns.now()
209
+ export const DatePicker = Object.assign(DatePicker_, {
210
+ isDateInPast: (date: Iso.Date) => dateFns.isBefore(date, today)
211
+ })