@strategicos/ui 0.8.0 → 0.9.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 (38) hide show
  1. package/README.md +23 -8
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  3. package/dist/components/date-input.d.ts +46 -0
  4. package/dist/components/date-input.d.ts.map +1 -0
  5. package/dist/components/date-input.js +202 -0
  6. package/dist/components/date-input.js.map +1 -0
  7. package/dist/components/date-picker-input.d.ts +49 -0
  8. package/dist/components/date-picker-input.d.ts.map +1 -0
  9. package/dist/components/date-picker-input.js +126 -0
  10. package/dist/components/date-picker-input.js.map +1 -0
  11. package/dist/components/date-time-picker.d.ts +27 -0
  12. package/dist/components/date-time-picker.d.ts.map +1 -0
  13. package/dist/components/date-time-picker.js +77 -0
  14. package/dist/components/date-time-picker.js.map +1 -0
  15. package/dist/components/dates.d.ts +94 -0
  16. package/dist/components/dates.d.ts.map +1 -0
  17. package/dist/components/dates.js +94 -0
  18. package/dist/components/dates.js.map +1 -0
  19. package/dist/components/popover.d.ts +1 -1
  20. package/dist/components/popover.d.ts.map +1 -1
  21. package/dist/components/popover.js +9 -2
  22. package/dist/components/popover.js.map +1 -1
  23. package/dist/components/time-input.d.ts +16 -0
  24. package/dist/components/time-input.d.ts.map +1 -0
  25. package/dist/components/time-input.js +18 -0
  26. package/dist/components/time-input.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/components/date-input.tsx +287 -0
  29. package/src/components/date-picker-input.tsx +287 -0
  30. package/src/components/date-time-picker.tsx +152 -0
  31. package/src/components/dates.tsx +274 -0
  32. package/src/components/popover.tsx +10 -0
  33. package/src/components/time-input.tsx +34 -0
  34. package/dist/components/date-picker.d.ts +0 -78
  35. package/dist/components/date-picker.d.ts.map +0 -1
  36. package/dist/components/date-picker.js +0 -142
  37. package/dist/components/date-picker.js.map +0 -1
  38. package/src/components/date-picker.tsx +0 -414
@@ -0,0 +1,287 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { CalendarIcon } from "lucide-react"
5
+
6
+ import { cn } from "../lib/utils.js"
7
+ import { Calendar } from "./calendar.js"
8
+ import {
9
+ clampDate, ClearButton, formatDate, formatDateTime, inRange, outOfRange,
10
+ parseDateValue, toDateValue, type FieldControlProps,
11
+ } from "./dates.js"
12
+ import { Input } from "./input.js"
13
+ import { Popover, PopoverAnchor, PopoverContent } from "./popover.js"
14
+
15
+ /** Põe as barras (e o espaço e os dois-pontos da hora) entre os dígitos.
16
+ * Separador só ENTRE grupos já começados: "15" e não "15/" — assim apagar
17
+ * para trás nunca esbarra num separador que volta sozinho. */
18
+ function mask(digits: string, withTime: boolean) {
19
+ const d = digits.slice(0, withTime ? 12 : 8)
20
+ let out = d.slice(0, 2)
21
+ if (d.length > 2) out += "/" + d.slice(2, 4)
22
+ if (d.length > 4) out += "/" + d.slice(4, 8)
23
+ if (d.length > 8) out += " " + d.slice(8, 10)
24
+ if (d.length > 10) out += ":" + d.slice(10, 12)
25
+ return out
26
+ }
27
+
28
+ const countDigits = (s: string) => s.replace(/\D/g, "").length
29
+
30
+ /** Posição do cursor logo depois do n-ésimo dígito do texto mascarado. */
31
+ function caretAfterDigits(masked: string, n: number) {
32
+ if (n === 0) return 0
33
+ let seen = 0
34
+ for (let i = 0; i < masked.length; i++) {
35
+ if (/\d/.test(masked[i]!) && ++seen === n) return i + 1
36
+ }
37
+ return masked.length
38
+ }
39
+
40
+ /**
41
+ * Texto digitado → valor. Data completa → "AAAA-MM-DD"; com hora →
42
+ * "AAAA-MM-DD HH:mm:00". Incompleto ou inexistente (31/02) → undefined.
43
+ * `dateOnlyOk`: com hora, aceita só a data (hora 00:00) — vale ao sair do
44
+ * campo, não enquanto se digita (a hora pode estar vindo).
45
+ */
46
+ function parseTyped(text: string, withTime: boolean, dateOnlyOk: boolean): string | undefined {
47
+ const d = text.replace(/\D/g, "")
48
+ if (d.length < 8) return undefined
49
+ const date = `${d.slice(4, 8)}-${d.slice(2, 4)}-${d.slice(0, 2)}`
50
+ let v: string
51
+ if (!withTime) v = date
52
+ else if (d.length === 12) v = `${date} ${d.slice(8, 10)}:${d.slice(10, 12)}:00`
53
+ else if (dateOnlyOk && d.length === 8) v = `${date} 00:00:00`
54
+ else return undefined
55
+ return parseDateValue(v) ? v : undefined
56
+ }
57
+
58
+ /**
59
+ * Data digitada, como o DateInput do Mantine: dd/mm/aaaa com as barras postas
60
+ * sozinhas, e um calendário que abre no clique (ou ↓) para quem prefere
61
+ * escolher. `withTime` acrescenta a hora: dd/mm/aaaa hh:mm.
62
+ *
63
+ * <Field label="Início">
64
+ * <DateInput value={inicio} onValueChange={setInicio} maxDate={fim} />
65
+ * </Field>
66
+ *
67
+ * - Valor: "AAAA-MM-DD" (com `withTime`, "AAAA-MM-DD HH:mm:ss") ou `null`.
68
+ * - Grava assim que o texto vira uma data válida dentro de `minDate`/`maxDate`.
69
+ * Ao sair do campo, texto incompleto ou fora dos limites volta ao último
70
+ * valor válido; campo vazio grava `null`.
71
+ * - Colar "2026-10-15" (ISO) também funciona.
72
+ *
73
+ * Teclado: ↓ abre o calendário e põe o foco nele; Enter confirma o texto; Esc
74
+ * fecha o calendário.
75
+ */
76
+ function DateInput({
77
+ value,
78
+ onValueChange,
79
+ withTime = false,
80
+ minDate,
81
+ maxDate,
82
+ placeholder = withTime ? "dd/mm/aaaa hh:mm" : "dd/mm/aaaa",
83
+ clearable = false,
84
+ withCalendar = true,
85
+ captionLayout,
86
+ disabled,
87
+ required,
88
+ name,
89
+ className,
90
+ wrapperClassName,
91
+ onBlur,
92
+ "aria-invalid": ariaInvalid,
93
+ ...props
94
+ }: FieldControlProps & {
95
+ value: string | null
96
+ onValueChange: (value: string | null) => void
97
+ /** Data e hora (dd/mm/aaaa hh:mm). */
98
+ withTime?: boolean
99
+ /** Primeiro dia aceito ("AAAA-MM-DD"). */
100
+ minDate?: string
101
+ /** Último dia aceito ("AAAA-MM-DD"). */
102
+ maxDate?: string
103
+ placeholder?: string
104
+ /** Um X no campo limpa o valor. */
105
+ clearable?: boolean
106
+ /** Calendário no clique. Sem ele, o campo é só de digitar. */
107
+ withCalendar?: boolean
108
+ captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years"
109
+ disabled?: boolean
110
+ required?: boolean
111
+ name?: string
112
+ /** Classes do <input>. */
113
+ className?: string
114
+ /** Classes do invólucro (largura e posição no layout). */
115
+ wrapperClassName?: string
116
+ onBlur?: React.FocusEventHandler<HTMLInputElement>
117
+ }) {
118
+ const display = React.useCallback((v: string | null) => {
119
+ const d = parseDateValue(v)
120
+ return d ? (withTime ? formatDateTime(d) : formatDate(d)) : ""
121
+ }, [withTime])
122
+
123
+ const [text, setText] = React.useState(() => display(value))
124
+ const [open, setOpen] = React.useState(false)
125
+ // Pedido de foco nos dias (↓). Contador, e não booleano: a ↓ com o
126
+ // calendário já aberto também precisa levar o foco.
127
+ const [focusRequest, setFocusRequest] = React.useState(0)
128
+ const [month, setMonth] = React.useState<Date | undefined>(() => parseDateValue(value))
129
+ const inputRef = React.useRef<HTMLInputElement>(null)
130
+ const anchorRef = React.useRef<HTMLDivElement>(null)
131
+ const contentRef = React.useRef<HTMLDivElement>(null)
132
+ const caret = React.useRef<number | null>(null)
133
+
134
+ // Valor trocado por fora (outro campo, reset do formulário): o texto segue,
135
+ // a menos que já represente esse valor (é o que se está digitando).
136
+ const [prevValue, setPrevValue] = React.useState(value)
137
+ if (value !== prevValue) {
138
+ setPrevValue(value)
139
+ if (parseTyped(text, withTime, true) !== value) setText(display(value))
140
+ }
141
+
142
+ // Recoloca o cursor onde estava depois que a máscara reescreveu o texto.
143
+ React.useLayoutEffect(() => {
144
+ if (caret.current === null || !inputRef.current) return
145
+ inputRef.current.setSelectionRange(caret.current, caret.current)
146
+ caret.current = null
147
+ }, [text])
148
+
149
+ const accepts = (v: string | undefined): v is string => !!v && inRange(v, minDate, maxDate)
150
+
151
+ function setTyped(next: string, caretAt: number | null) {
152
+ caret.current = caretAt
153
+ setText(next)
154
+ const v = parseTyped(next, withTime, false)
155
+ if (accepts(v)) {
156
+ setMonth(parseDateValue(v))
157
+ if (v !== value) onValueChange(v)
158
+ }
159
+ }
160
+
161
+ function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
162
+ const raw = e.target.value
163
+ const before = countDigits(raw.slice(0, e.target.selectionStart ?? raw.length))
164
+ const next = mask(raw.replace(/\D/g, ""), withTime)
165
+ setTyped(next, caretAfterDigits(next, before))
166
+ }
167
+
168
+ function handlePaste(e: React.ClipboardEvent<HTMLInputElement>) {
169
+ // ISO colado ("2026-10-15", "2026-10-15 14:30") vira o texto brasileiro.
170
+ const d = parseDateValue(e.clipboardData.getData("text").trim())
171
+ if (!d) return
172
+ e.preventDefault()
173
+ setTyped(withTime ? formatDateTime(d) : formatDate(d), null)
174
+ }
175
+
176
+ /** Sair do campo (ou Enter): grava o texto válido ou volta ao último valor. */
177
+ function settle() {
178
+ if (!text.trim()) {
179
+ if (value !== null) onValueChange(null)
180
+ return
181
+ }
182
+ const v = parseTyped(text, withTime, true)
183
+ if (accepts(v)) {
184
+ if (v !== value) onValueChange(v)
185
+ setText(display(v))
186
+ } else {
187
+ setText(display(value))
188
+ }
189
+ }
190
+
191
+ // O dia focável do DayPicker é o único com tabindex=0 (o escolhido ou hoje).
192
+ React.useEffect(() => {
193
+ if (!focusRequest) return
194
+ contentRef.current?.querySelector<HTMLButtonElement>('[role=grid] button[tabindex="0"]')?.focus()
195
+ }, [focusRequest])
196
+
197
+ function openCalendar(focus: boolean) {
198
+ if (!withCalendar || disabled) return
199
+ if (!open) setMonth(parseDateValue(value))
200
+ setOpen(true)
201
+ if (focus) setFocusRequest(n => n + 1)
202
+ }
203
+
204
+ function pickDay(day: Date) {
205
+ // Com hora, mantém a hora já digitada (ou a do valor, ou 00:00).
206
+ const current = parseTyped(text, withTime, true) ?? value
207
+ const time = current && current.length >= 19 ? current.slice(11, 19) : "00:00:00"
208
+ const v = withTime ? `${toDateValue(day)} ${time}` : toDateValue(day)
209
+ onValueChange(v)
210
+ setText(display(v))
211
+ setOpen(false)
212
+ inputRef.current?.focus()
213
+ }
214
+
215
+ // Digitado e completo, mas inválido ou fora dos limites: borda de erro já
216
+ // enquanto se digita (o Field, se houver, ainda manda no aria-invalid).
217
+ const typedInvalid = countDigits(text) === (withTime ? 12 : 8) && !accepts(parseTyped(text, withTime, false))
218
+ const min = parseDateValue(minDate)
219
+ const max = parseDateValue(maxDate)
220
+ const selected = parseDateValue(value)
221
+ const showClear = clearable && !!text && !disabled
222
+
223
+ return (
224
+ <Popover open={open} onOpenChange={setOpen}>
225
+ <PopoverAnchor asChild>
226
+ <div ref={anchorRef} data-slot="date-input-wrapper" className={cn("relative w-full min-w-0", wrapperClassName)}>
227
+ <Input
228
+ ref={inputRef}
229
+ data-slot="date-input"
230
+ value={text}
231
+ placeholder={placeholder}
232
+ disabled={disabled}
233
+ required={required}
234
+ name={name}
235
+ inputMode="numeric"
236
+ autoComplete="off"
237
+ aria-haspopup={withCalendar ? "dialog" : undefined}
238
+ aria-expanded={withCalendar ? open : undefined}
239
+ aria-invalid={ariaInvalid ?? (typedInvalid || undefined)}
240
+ leftSection={<CalendarIcon />}
241
+ className={cn("tabular-nums", showClear && "pr-9", className)}
242
+ onChange={handleChange}
243
+ onPaste={handlePaste}
244
+ onClick={() => { if (!open) openCalendar(false) }}
245
+ onKeyDown={e => {
246
+ if (e.key === "ArrowDown" && withCalendar) { e.preventDefault(); openCalendar(true) }
247
+ else if (e.key === "Enter") { e.preventDefault(); settle(); setOpen(false) }
248
+ }}
249
+ onBlur={e => { settle(); onBlur?.(e) }}
250
+ {...props}
251
+ />
252
+ {showClear && (
253
+ <ClearButton onClick={() => { setText(""); onValueChange(null); inputRef.current?.focus() }} />
254
+ )}
255
+ </div>
256
+ </PopoverAnchor>
257
+
258
+ {withCalendar && (
259
+ <PopoverContent
260
+ ref={contentRef}
261
+ className="w-auto p-0"
262
+ // O foco fica no campo: dá para continuar digitando com o calendário
263
+ // aberto. Só a ↓ leva o foco para os dias.
264
+ onOpenAutoFocus={e => e.preventDefault()}
265
+ onCloseAutoFocus={e => e.preventDefault()}
266
+ // Clicar no próprio campo não conta como "fora".
267
+ onInteractOutside={e => { if (anchorRef.current?.contains(e.target as Node)) e.preventDefault() }}
268
+ >
269
+ <Calendar
270
+ mode="single"
271
+ required
272
+ selected={selected}
273
+ onSelect={pickDay}
274
+ month={month ?? clampDate(new Date(), min, max)}
275
+ onMonthChange={setMonth}
276
+ startMonth={min}
277
+ endMonth={max}
278
+ disabled={outOfRange(min, max)}
279
+ captionLayout={captionLayout}
280
+ />
281
+ </PopoverContent>
282
+ )}
283
+ </Popover>
284
+ )
285
+ }
286
+
287
+ export { DateInput }
@@ -0,0 +1,287 @@
1
+ "use client"
2
+
3
+ // Derivado do DatePicker do Tremor (Apache-2.0) e modificado pela Strategicos.
4
+ // Ver THIRD_PARTY_NOTICES.md.
5
+
6
+ import * as React from "react"
7
+
8
+ import { useIsMobile } from "../hooks/use-mobile.js"
9
+ import { Calendar } from "./calendar.js"
10
+ import {
11
+ clampDate, formatDate, inRange, outOfRange, parseDateValue, toDateValue,
12
+ PickerFooter, PickerHint, PickerTrigger, PresetList, type PickerBaseProps,
13
+ } from "./dates.js"
14
+ import { useFieldControl } from "./field.js"
15
+ import { Popover, PopoverContent } from "./popover.js"
16
+
17
+ export type DatePreset = { label: string; value: string }
18
+ export type DateRangeValue = [string | null, string | null]
19
+ export type DateRangePreset = { label: string; value: [string, string] }
20
+
21
+ type SingleProps = PickerBaseProps & {
22
+ type?: "default"
23
+ value: string | null
24
+ onValueChange: (value: string | null) => void
25
+ presets?: DatePreset[]
26
+ }
27
+
28
+ type RangeProps = PickerBaseProps & {
29
+ type: "range"
30
+ value: DateRangeValue
31
+ onValueChange: (value: DateRangeValue) => void
32
+ presets?: DateRangePreset[]
33
+ /** Meses lado a lado (no celular é sempre um). Padrão: 2. */
34
+ numberOfColumns?: number
35
+ }
36
+
37
+ const contentCls = "flex w-auto max-w-[calc(100vw-2rem)] flex-col p-0 sm:flex-row"
38
+
39
+ /**
40
+ * Campo que abre um calendário para escolher uma data ou um período, como o
41
+ * DatePickerInput do Mantine. Não se digita nele — para digitar, `DateInput`.
42
+ *
43
+ * <Field label="Prazo">
44
+ * <DatePickerInput value={prazo} onValueChange={setPrazo} minDate={inicio} clearable />
45
+ * </Field>
46
+ *
47
+ * <DatePickerInput type="range" value={[inicio, fim]} onValueChange={([i, f]) => …} withApplyButton />
48
+ *
49
+ * - Valor: "AAAA-MM-DD" (ou `[início, fim]` no `type="range"`), `null` quando vazio.
50
+ * - `withApplyButton`: a escolha só vale no Aplicar; fechar de outro jeito
51
+ * descarta. Sem ele, escolher o dia (ou o fim do período) já grava e fecha.
52
+ * - `presets`: atalhos ao lado ("Hoje", "Fim do mês"…); os que caem fora de
53
+ * `minDate`/`maxDate` ficam desabilitados.
54
+ * - Período: o primeiro clique marca o início e o segundo, o fim; se o segundo
55
+ * vier antes, os dois trocam.
56
+ *
57
+ * Teclado: o calendário abre com o foco no dia escolhido (ou hoje); setas
58
+ * andam pelos dias, PageUp/PageDown pelos meses, Enter escolhe, Esc fecha.
59
+ */
60
+ function DatePickerInput(props: SingleProps | RangeProps) {
61
+ return props.type === "range" ? <RangePicker {...props} /> : <SinglePicker {...props} />
62
+ }
63
+
64
+ function SinglePicker({
65
+ type: _type,
66
+ value,
67
+ onValueChange,
68
+ placeholder = "Selecione a data",
69
+ minDate,
70
+ maxDate,
71
+ clearable = false,
72
+ withApplyButton = false,
73
+ presets,
74
+ valueFormatter = formatDate,
75
+ captionLayout,
76
+ size,
77
+ disabled,
78
+ className,
79
+ wrapperClassName,
80
+ align = "start",
81
+ onOpenChange,
82
+ ...props
83
+ }: SingleProps) {
84
+ const controlProps = useFieldControl(props, { withRequired: false })
85
+ const [open, setOpen] = React.useState(false)
86
+ // Com o Aplicar, a escolha fica aqui até confirmar.
87
+ const [draft, setDraft] = React.useState<string | null>(value)
88
+ const shown = withApplyButton ? draft : value
89
+
90
+ const selected = parseDateValue(shown)
91
+ const committed = parseDateValue(value)
92
+ const min = parseDateValue(minDate)
93
+ const max = parseDateValue(maxDate)
94
+
95
+ function handleOpenChange(o: boolean) {
96
+ setOpen(o)
97
+ if (o) setDraft(value)
98
+ onOpenChange?.(o)
99
+ }
100
+
101
+ function pick(v: string) {
102
+ if (withApplyButton) { setDraft(v); return }
103
+ onValueChange(v)
104
+ handleOpenChange(false)
105
+ }
106
+
107
+ function apply() {
108
+ if (draft !== value) onValueChange(draft)
109
+ handleOpenChange(false)
110
+ }
111
+
112
+ return (
113
+ <Popover open={open} onOpenChange={handleOpenChange}>
114
+ <PickerTrigger
115
+ slot="date-picker-input"
116
+ text={committed ? valueFormatter(committed) : null}
117
+ placeholder={placeholder}
118
+ open={open}
119
+ size={size}
120
+ disabled={disabled}
121
+ clearable={clearable}
122
+ onClear={() => onValueChange(null)}
123
+ className={className}
124
+ wrapperClassName={wrapperClassName}
125
+ controlProps={controlProps}
126
+ />
127
+ <PopoverContent
128
+ align={align}
129
+ className={contentCls}
130
+ // O foco vai para o dia (autoFocus do calendário), não para o 1º botão.
131
+ onOpenAutoFocus={e => e.preventDefault()}
132
+ >
133
+ {presets && presets.length > 0 && (
134
+ <PresetList
135
+ presets={presets}
136
+ isActive={v => v === shown}
137
+ isDisabled={v => !inRange(v, minDate, maxDate)}
138
+ onSelect={pick}
139
+ />
140
+ )}
141
+ <div className="flex flex-col">
142
+ <Calendar
143
+ mode="single"
144
+ required
145
+ autoFocus
146
+ selected={selected}
147
+ onSelect={d => pick(toDateValue(d))}
148
+ defaultMonth={selected ?? clampDate(new Date(), min, max)}
149
+ startMonth={min}
150
+ endMonth={max}
151
+ disabled={outOfRange(min, max)}
152
+ captionLayout={captionLayout}
153
+ />
154
+ {withApplyButton && (
155
+ <PickerFooter onCancel={() => handleOpenChange(false)} onApply={apply}>
156
+ <PickerHint>{selected ? valueFormatter(selected) : "Nenhuma data"}</PickerHint>
157
+ </PickerFooter>
158
+ )}
159
+ </div>
160
+ </PopoverContent>
161
+ </Popover>
162
+ )
163
+ }
164
+
165
+ function RangePicker({
166
+ type: _type,
167
+ value,
168
+ onValueChange,
169
+ placeholder = "Selecione o período",
170
+ minDate,
171
+ maxDate,
172
+ clearable = false,
173
+ withApplyButton = false,
174
+ presets,
175
+ numberOfColumns = 2,
176
+ valueFormatter = formatDate,
177
+ captionLayout,
178
+ size,
179
+ disabled,
180
+ className,
181
+ wrapperClassName,
182
+ align = "start",
183
+ onOpenChange,
184
+ ...props
185
+ }: RangeProps) {
186
+ const controlProps = useFieldControl(props, { withRequired: false })
187
+ const isMobile = useIsMobile()
188
+ const [open, setOpen] = React.useState(false)
189
+ const [draft, setDraft] = React.useState<DateRangeValue>(value)
190
+ // Início escolhido nesta abertura, à espera do fim.
191
+ const [pendingFrom, setPendingFrom] = React.useState<Date | null>(null)
192
+ const shown = withApplyButton ? draft : value
193
+
194
+ const from = parseDateValue(shown[0])
195
+ const to = parseDateValue(shown[1])
196
+ const min = parseDateValue(minDate)
197
+ const max = parseDateValue(maxDate)
198
+
199
+ const format = (range: DateRangeValue) => {
200
+ const f = parseDateValue(range[0])
201
+ const t = parseDateValue(range[1])
202
+ return f ? `${valueFormatter(f)} – ${t ? valueFormatter(t) : "…"}` : null
203
+ }
204
+
205
+ function handleOpenChange(o: boolean) {
206
+ setOpen(o)
207
+ setPendingFrom(null)
208
+ if (o) setDraft(value)
209
+ onOpenChange?.(o)
210
+ }
211
+
212
+ function commit(range: [string, string]) {
213
+ if (withApplyButton) { setDraft(range); setPendingFrom(null); return }
214
+ onValueChange(range)
215
+ handleOpenChange(false)
216
+ }
217
+
218
+ function pickDay(day: Date) {
219
+ if (!pendingFrom) { setPendingFrom(day); return }
220
+ const [s, e] = day < pendingFrom ? [day, pendingFrom] : [pendingFrom, day]
221
+ commit([toDateValue(s), toDateValue(e)])
222
+ }
223
+
224
+ function apply() {
225
+ if (draft[0] !== value[0] || draft[1] !== value[1]) onValueChange(draft)
226
+ handleOpenChange(false)
227
+ }
228
+
229
+ const hint = pendingFrom
230
+ ? "Agora escolha o último dia."
231
+ : withApplyButton && from
232
+ ? format(shown)
233
+ : "Escolha o primeiro dia."
234
+
235
+ return (
236
+ <Popover open={open} onOpenChange={handleOpenChange}>
237
+ <PickerTrigger
238
+ slot="date-picker-input"
239
+ text={format(value)}
240
+ placeholder={placeholder}
241
+ open={open}
242
+ size={size}
243
+ disabled={disabled}
244
+ clearable={clearable}
245
+ onClear={() => onValueChange([null, null])}
246
+ className={className}
247
+ wrapperClassName={wrapperClassName}
248
+ controlProps={controlProps}
249
+ />
250
+ <PopoverContent align={align} className={contentCls} onOpenAutoFocus={e => e.preventDefault()}>
251
+ {presets && presets.length > 0 && (
252
+ <PresetList
253
+ presets={presets}
254
+ isActive={v => v[0] === shown[0] && v[1] === shown[1]}
255
+ isDisabled={v => !inRange(v[0], minDate, maxDate) || !inRange(v[1], minDate, maxDate)}
256
+ onSelect={commit}
257
+ />
258
+ )}
259
+ <div className="flex flex-col">
260
+ <Calendar
261
+ mode="range"
262
+ autoFocus
263
+ numberOfMonths={isMobile ? 1 : numberOfColumns}
264
+ // O cálculo do período é nosso (pickDay): o do DayPicker já fecha
265
+ // um período de um dia no primeiro clique.
266
+ selected={pendingFrom ? { from: pendingFrom, to: undefined } : { from, to }}
267
+ onSelect={(_, day) => pickDay(day)}
268
+ defaultMonth={from ?? clampDate(new Date(), min, max)}
269
+ startMonth={min}
270
+ endMonth={max}
271
+ disabled={outOfRange(min, max)}
272
+ captionLayout={captionLayout}
273
+ />
274
+ <PickerFooter
275
+ onCancel={withApplyButton ? () => handleOpenChange(false) : undefined}
276
+ onApply={withApplyButton ? apply : undefined}
277
+ applyDisabled={!!pendingFrom}
278
+ >
279
+ <PickerHint>{hint}</PickerHint>
280
+ </PickerFooter>
281
+ </div>
282
+ </PopoverContent>
283
+ </Popover>
284
+ )
285
+ }
286
+
287
+ export { DatePickerInput }