@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.
- package/README.md +23 -8
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/dist/components/date-input.d.ts +46 -0
- package/dist/components/date-input.d.ts.map +1 -0
- package/dist/components/date-input.js +202 -0
- package/dist/components/date-input.js.map +1 -0
- package/dist/components/date-picker-input.d.ts +49 -0
- package/dist/components/date-picker-input.d.ts.map +1 -0
- package/dist/components/date-picker-input.js +126 -0
- package/dist/components/date-picker-input.js.map +1 -0
- package/dist/components/date-time-picker.d.ts +27 -0
- package/dist/components/date-time-picker.d.ts.map +1 -0
- package/dist/components/date-time-picker.js +77 -0
- package/dist/components/date-time-picker.js.map +1 -0
- package/dist/components/dates.d.ts +94 -0
- package/dist/components/dates.d.ts.map +1 -0
- package/dist/components/dates.js +94 -0
- package/dist/components/dates.js.map +1 -0
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.d.ts.map +1 -1
- package/dist/components/popover.js +9 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/time-input.d.ts +16 -0
- package/dist/components/time-input.d.ts.map +1 -0
- package/dist/components/time-input.js +18 -0
- package/dist/components/time-input.js.map +1 -0
- package/package.json +1 -1
- package/src/components/date-input.tsx +287 -0
- package/src/components/date-picker-input.tsx +287 -0
- package/src/components/date-time-picker.tsx +152 -0
- package/src/components/dates.tsx +274 -0
- package/src/components/popover.tsx +10 -0
- package/src/components/time-input.tsx +34 -0
- package/dist/components/date-picker.d.ts +0 -78
- package/dist/components/date-picker.d.ts.map +0 -1
- package/dist/components/date-picker.js +0 -142
- package/dist/components/date-picker.js.map +0 -1
- package/src/components/date-picker.tsx +0 -414
|
@@ -0,0 +1,152 @@
|
|
|
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 { Calendar } from "./calendar.js"
|
|
9
|
+
import {
|
|
10
|
+
clampDate, formatDateTime, outOfRange, parseDateValue, toDateValue,
|
|
11
|
+
PickerFooter, PickerTrigger, type PickerBaseProps,
|
|
12
|
+
} from "./dates.js"
|
|
13
|
+
import { useFieldControl } from "./field.js"
|
|
14
|
+
import { Popover, PopoverContent } from "./popover.js"
|
|
15
|
+
import { TimeInput } from "./time-input.js"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Data e hora num calendário com campo de hora, como o DateTimePicker do
|
|
19
|
+
* Mantine. Para digitar a data e a hora direto, `DateInput withTime`.
|
|
20
|
+
*
|
|
21
|
+
* <Field label="Reunião">
|
|
22
|
+
* <DateTimePicker value={inicio} onValueChange={setInicio} />
|
|
23
|
+
* </Field>
|
|
24
|
+
*
|
|
25
|
+
* - Valor: "AAAA-MM-DD HH:mm:ss" ou `null`.
|
|
26
|
+
* - `withApplyButton` vem LIGADO (o botão de confirmar do Mantine): escolhe-se
|
|
27
|
+
* o dia e a hora e grava-se no Aplicar. Desligado, cada mudança já grava e o
|
|
28
|
+
* calendário fica aberto até sair dele.
|
|
29
|
+
* - `defaultTime`: hora usada quando ainda não há valor. Padrão: 00:00.
|
|
30
|
+
* - `minDate`/`maxDate` limitam o DIA ("AAAA-MM-DD").
|
|
31
|
+
*/
|
|
32
|
+
function DateTimePicker({
|
|
33
|
+
value,
|
|
34
|
+
onValueChange,
|
|
35
|
+
placeholder = "Selecione data e hora",
|
|
36
|
+
minDate,
|
|
37
|
+
maxDate,
|
|
38
|
+
clearable = false,
|
|
39
|
+
withApplyButton = true,
|
|
40
|
+
withSeconds = false,
|
|
41
|
+
defaultTime = "00:00",
|
|
42
|
+
valueFormatter = formatDateTime,
|
|
43
|
+
captionLayout,
|
|
44
|
+
size,
|
|
45
|
+
disabled,
|
|
46
|
+
className,
|
|
47
|
+
wrapperClassName,
|
|
48
|
+
align = "start",
|
|
49
|
+
onOpenChange,
|
|
50
|
+
...props
|
|
51
|
+
}: PickerBaseProps & {
|
|
52
|
+
value: string | null
|
|
53
|
+
onValueChange: (value: string | null) => void
|
|
54
|
+
/** Campo de hora com segundos. */
|
|
55
|
+
withSeconds?: boolean
|
|
56
|
+
/** "HH:mm" usada quando ainda não há valor. */
|
|
57
|
+
defaultTime?: string
|
|
58
|
+
}) {
|
|
59
|
+
const controlProps = useFieldControl(props, { withRequired: false })
|
|
60
|
+
const [open, setOpen] = React.useState(false)
|
|
61
|
+
const [draft, setDraft] = React.useState<string | null>(value)
|
|
62
|
+
// A hora existe antes do dia: dá para acertá-la e escolher o dia depois.
|
|
63
|
+
const [time, setTime] = React.useState(() => timeOf(value, defaultTime))
|
|
64
|
+
const shown = withApplyButton ? draft : value
|
|
65
|
+
|
|
66
|
+
const selected = parseDateValue(shown)
|
|
67
|
+
const committed = parseDateValue(value)
|
|
68
|
+
const min = parseDateValue(minDate)
|
|
69
|
+
const max = parseDateValue(maxDate)
|
|
70
|
+
|
|
71
|
+
function handleOpenChange(o: boolean) {
|
|
72
|
+
setOpen(o)
|
|
73
|
+
if (o) { setDraft(value); setTime(timeOf(value, defaultTime)) }
|
|
74
|
+
onOpenChange?.(o)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function update(v: string) {
|
|
78
|
+
if (withApplyButton) setDraft(v)
|
|
79
|
+
else onValueChange(v)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// `time` é sempre "HH:mm:ss" (timeOf e changeTime completam).
|
|
83
|
+
function pickDay(day: Date) {
|
|
84
|
+
update(`${toDateValue(day)} ${time}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function changeTime(t: string) {
|
|
88
|
+
// O campo nativo devolve "" enquanto a hora está pela metade.
|
|
89
|
+
if (!/^\d{2}:\d{2}(:\d{2})?$/.test(t)) return
|
|
90
|
+
const full = t.length === 5 ? `${t}:00` : t
|
|
91
|
+
setTime(full)
|
|
92
|
+
if (shown) update(`${shown.slice(0, 10)} ${full}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function apply() {
|
|
96
|
+
if (draft !== value) onValueChange(draft)
|
|
97
|
+
handleOpenChange(false)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
102
|
+
<PickerTrigger
|
|
103
|
+
slot="date-time-picker"
|
|
104
|
+
text={committed ? valueFormatter(committed) : null}
|
|
105
|
+
placeholder={placeholder}
|
|
106
|
+
open={open}
|
|
107
|
+
size={size}
|
|
108
|
+
disabled={disabled}
|
|
109
|
+
clearable={clearable}
|
|
110
|
+
onClear={() => onValueChange(null)}
|
|
111
|
+
className={className}
|
|
112
|
+
wrapperClassName={wrapperClassName}
|
|
113
|
+
controlProps={controlProps}
|
|
114
|
+
/>
|
|
115
|
+
<PopoverContent align={align} className="w-auto max-w-[calc(100vw-2rem)] p-0" onOpenAutoFocus={e => e.preventDefault()}>
|
|
116
|
+
<Calendar
|
|
117
|
+
mode="single"
|
|
118
|
+
required
|
|
119
|
+
autoFocus
|
|
120
|
+
selected={selected}
|
|
121
|
+
onSelect={pickDay}
|
|
122
|
+
defaultMonth={selected ?? clampDate(new Date(), min, max)}
|
|
123
|
+
startMonth={min}
|
|
124
|
+
endMonth={max}
|
|
125
|
+
disabled={outOfRange(min, max)}
|
|
126
|
+
captionLayout={captionLayout}
|
|
127
|
+
/>
|
|
128
|
+
<PickerFooter
|
|
129
|
+
onCancel={withApplyButton ? () => handleOpenChange(false) : undefined}
|
|
130
|
+
onApply={withApplyButton ? apply : undefined}
|
|
131
|
+
applyDisabled={!draft}
|
|
132
|
+
>
|
|
133
|
+
<TimeInput
|
|
134
|
+
aria-label="Hora"
|
|
135
|
+
withSeconds={withSeconds}
|
|
136
|
+
value={withSeconds ? time : time.slice(0, 5)}
|
|
137
|
+
onChange={e => changeTime(e.target.value)}
|
|
138
|
+
className="w-32"
|
|
139
|
+
/>
|
|
140
|
+
</PickerFooter>
|
|
141
|
+
</PopoverContent>
|
|
142
|
+
</Popover>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** "HH:mm:ss" do valor, ou a hora padrão. */
|
|
147
|
+
function timeOf(value: string | null, defaultTime: string) {
|
|
148
|
+
const t = value && value.length >= 16 ? value.slice(11, 19) : defaultTime
|
|
149
|
+
return t.length === 5 ? `${t}:00` : t
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { DateTimePicker }
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
// Peças derivadas do DatePicker do Tremor (Apache-2.0) e modificadas pela
|
|
4
|
+
// Strategicos. Ver THIRD_PARTY_NOTICES.md.
|
|
5
|
+
|
|
6
|
+
import * as React from "react"
|
|
7
|
+
import { CalendarIcon, XIcon } from "lucide-react"
|
|
8
|
+
import type { Matcher } from "react-day-picker"
|
|
9
|
+
|
|
10
|
+
import { cn } from "../lib/utils.js"
|
|
11
|
+
import { Button } from "./button.js"
|
|
12
|
+
import { PopoverTrigger } from "./popover.js"
|
|
13
|
+
import { selectTriggerVariants } from "./select.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Utilitários dos campos de data (DateInput, DatePickerInput, DateTimePicker).
|
|
17
|
+
*
|
|
18
|
+
* Os valores seguem o Mantine 8: texto, nunca `Date`.
|
|
19
|
+
* - data: "AAAA-MM-DD" (como o <input type="date">)
|
|
20
|
+
* - data e hora: "AAAA-MM-DD HH:mm:ss"
|
|
21
|
+
* Sem fuso: 2026-10-15 é 15/10 em qualquer lugar. Um Date à meia-noite UTC
|
|
22
|
+
* vira o dia 14 no Brasil.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/
|
|
26
|
+
|
|
27
|
+
/** "AAAA-MM-DD" ou "AAAA-MM-DD HH:mm[:ss]" → Date local. Vazio ou inválido →
|
|
28
|
+
* undefined. */
|
|
29
|
+
function parseDateValue(value: string | null | undefined): Date | undefined {
|
|
30
|
+
const m = value ? DATE_RE.exec(value) : null
|
|
31
|
+
if (!m) return undefined
|
|
32
|
+
const [y, mo, d, h = "0", mi = "0", s = "0"] = m.slice(1)
|
|
33
|
+
const date = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s))
|
|
34
|
+
// 2026-02-31 não existe: o Date rolaria para março.
|
|
35
|
+
if (date.getMonth() !== Number(mo) - 1 || Number(h) > 23 || Number(mi) > 59 || Number(s) > 59) return undefined
|
|
36
|
+
return date
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
40
|
+
|
|
41
|
+
/** Date → "AAAA-MM-DD" (dia local). */
|
|
42
|
+
function toDateValue(date: Date): string {
|
|
43
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Date → "AAAA-MM-DD HH:mm:ss" (hora local). */
|
|
47
|
+
function toDateTimeValue(date: Date): string {
|
|
48
|
+
return `${toDateValue(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 15/10/2026 */
|
|
52
|
+
const formatDate = (date: Date) => date.toLocaleDateString("pt-BR")
|
|
53
|
+
|
|
54
|
+
/** 15/10/2026 14:30 */
|
|
55
|
+
const formatDateTime = (date: Date) => `${formatDate(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
56
|
+
|
|
57
|
+
// ── Limites ───────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function clampDate(date: Date, min?: Date, max?: Date) {
|
|
60
|
+
if (min && date < min) return min
|
|
61
|
+
if (max && date > max) return max
|
|
62
|
+
return date
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Dias fora de [min, max] para o `disabled` do Calendar. */
|
|
66
|
+
function outOfRange(min?: Date, max?: Date): Matcher[] {
|
|
67
|
+
const m: Matcher[] = []
|
|
68
|
+
if (min) m.push({ before: min })
|
|
69
|
+
if (max) m.push({ after: max })
|
|
70
|
+
return m
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Compara só o dia ("AAAA-MM-DD…" ordena como texto). */
|
|
74
|
+
function inRange(value: string, minDate?: string, maxDate?: string) {
|
|
75
|
+
const day = value.slice(0, 10)
|
|
76
|
+
return (!minDate || day >= minDate.slice(0, 10)) && (!maxDate || day <= maxDate.slice(0, 10))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Peças comuns ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
type FieldControlProps = {
|
|
82
|
+
id?: string
|
|
83
|
+
"aria-label"?: string
|
|
84
|
+
"aria-describedby"?: string
|
|
85
|
+
"aria-invalid"?: React.AriaAttributes["aria-invalid"]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Gatilho dos pickers: o visual do SelectTrigger, com ícone de calendário e,
|
|
89
|
+
* opcionalmente, um X para limpar. Vai dentro de um <Popover>. */
|
|
90
|
+
function PickerTrigger({
|
|
91
|
+
text,
|
|
92
|
+
placeholder,
|
|
93
|
+
open,
|
|
94
|
+
size = "default",
|
|
95
|
+
disabled,
|
|
96
|
+
clearable,
|
|
97
|
+
onClear,
|
|
98
|
+
className,
|
|
99
|
+
wrapperClassName,
|
|
100
|
+
slot,
|
|
101
|
+
controlProps,
|
|
102
|
+
}: {
|
|
103
|
+
text: string | null
|
|
104
|
+
placeholder: string
|
|
105
|
+
open: boolean
|
|
106
|
+
size?: "sm" | "default" | null
|
|
107
|
+
disabled?: boolean
|
|
108
|
+
clearable?: boolean
|
|
109
|
+
onClear: () => void
|
|
110
|
+
className?: string
|
|
111
|
+
wrapperClassName?: string
|
|
112
|
+
slot: string
|
|
113
|
+
controlProps: FieldControlProps
|
|
114
|
+
}) {
|
|
115
|
+
const showClear = clearable && !!text && !disabled
|
|
116
|
+
|
|
117
|
+
const trigger = (
|
|
118
|
+
<PopoverTrigger asChild disabled={disabled}>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
aria-haspopup="dialog"
|
|
122
|
+
aria-expanded={open}
|
|
123
|
+
data-slot={slot}
|
|
124
|
+
data-size={size ?? "default"}
|
|
125
|
+
data-placeholder={text ? undefined : ""}
|
|
126
|
+
{...controlProps}
|
|
127
|
+
className={cn(
|
|
128
|
+
selectTriggerVariants({ size }),
|
|
129
|
+
"w-full min-w-0 cursor-pointer justify-start text-left tabular-nums",
|
|
130
|
+
showClear && "pr-9",
|
|
131
|
+
className
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<CalendarIcon className="text-muted-foreground" />
|
|
135
|
+
<span className="min-w-0 flex-1 truncate">{text ?? placeholder}</span>
|
|
136
|
+
</button>
|
|
137
|
+
</PopoverTrigger>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if (!clearable) return trigger
|
|
141
|
+
return (
|
|
142
|
+
<div data-slot={`${slot}-wrapper`} className={cn("relative w-full min-w-0", wrapperClassName)}>
|
|
143
|
+
{trigger}
|
|
144
|
+
{showClear && <ClearButton onClick={onClear} />}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** X de limpar. Fica ao lado do gatilho, não dentro: botão dentro de botão não
|
|
150
|
+
* é HTML válido. */
|
|
151
|
+
function ClearButton({ onClick, className }: { onClick: () => void; className?: string }) {
|
|
152
|
+
return (
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
onClick={onClick}
|
|
156
|
+
aria-label="Limpar data"
|
|
157
|
+
className={cn(
|
|
158
|
+
"absolute inset-y-0 right-1.5 my-auto flex size-6 cursor-pointer items-center justify-center rounded text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50",
|
|
159
|
+
className
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<XIcon className="size-4" />
|
|
163
|
+
</button>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Atalhos ao lado do calendário (no celular, uma faixa em cima). */
|
|
168
|
+
function PresetList<T>({
|
|
169
|
+
presets,
|
|
170
|
+
isActive,
|
|
171
|
+
isDisabled,
|
|
172
|
+
onSelect,
|
|
173
|
+
}: {
|
|
174
|
+
presets: { label: string; value: T }[]
|
|
175
|
+
isActive: (value: T) => boolean
|
|
176
|
+
isDisabled: (value: T) => boolean
|
|
177
|
+
onSelect: (value: T) => void
|
|
178
|
+
}) {
|
|
179
|
+
return (
|
|
180
|
+
<ul
|
|
181
|
+
data-slot="date-picker-presets"
|
|
182
|
+
className="flex gap-1 overflow-x-auto border-b border-border p-2 sm:w-40 sm:flex-col sm:overflow-visible sm:border-r sm:border-b-0"
|
|
183
|
+
>
|
|
184
|
+
{presets.map(p => (
|
|
185
|
+
<li key={p.label} className="shrink-0">
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
disabled={isDisabled(p.value)}
|
|
189
|
+
aria-pressed={isActive(p.value)}
|
|
190
|
+
onClick={() => onSelect(p.value)}
|
|
191
|
+
className="w-full cursor-pointer truncate rounded-md px-2.5 py-1.5 text-left text-sm whitespace-nowrap outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-pressed:bg-accent aria-pressed:font-medium"
|
|
192
|
+
>
|
|
193
|
+
{p.label}
|
|
194
|
+
</button>
|
|
195
|
+
</li>
|
|
196
|
+
))}
|
|
197
|
+
</ul>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Rodapé do calendário: algo à esquerda (dica, hora) e, com
|
|
202
|
+
* `withApplyButton`, Cancelar e Aplicar à direita. Para texto de dica, use
|
|
203
|
+
* <PickerHint>; o rodapé não tinge o que vai nele (a hora é um campo). */
|
|
204
|
+
function PickerFooter({
|
|
205
|
+
children,
|
|
206
|
+
onCancel,
|
|
207
|
+
onApply,
|
|
208
|
+
applyDisabled,
|
|
209
|
+
}: {
|
|
210
|
+
children?: React.ReactNode
|
|
211
|
+
onCancel?: () => void
|
|
212
|
+
onApply?: () => void
|
|
213
|
+
applyDisabled?: boolean
|
|
214
|
+
}) {
|
|
215
|
+
return (
|
|
216
|
+
<div data-slot="date-picker-footer" className="flex flex-wrap items-center gap-2 border-t border-border p-3">
|
|
217
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
218
|
+
{onApply && (
|
|
219
|
+
<div className="flex shrink-0 gap-2">
|
|
220
|
+
<Button type="button" variant="ghost" onClick={onCancel}>Cancelar</Button>
|
|
221
|
+
<Button type="button" onClick={onApply} disabled={applyDisabled}>Aplicar</Button>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function PickerHint({ children }: { children: React.ReactNode }) {
|
|
229
|
+
return <span aria-live="polite" className="text-xs text-muted-foreground">{children}</span>
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Props comuns aos pickers (DatePickerInput e DateTimePicker). */
|
|
233
|
+
type PickerBaseProps = FieldControlProps & {
|
|
234
|
+
placeholder?: string
|
|
235
|
+
/** Primeiro dia que pode ser escolhido ("AAAA-MM-DD"). */
|
|
236
|
+
minDate?: string
|
|
237
|
+
/** Último dia que pode ser escolhido ("AAAA-MM-DD"). */
|
|
238
|
+
maxDate?: string
|
|
239
|
+
/** Um X no gatilho limpa o valor. */
|
|
240
|
+
clearable?: boolean
|
|
241
|
+
/** Rodapé com Cancelar e Aplicar: a escolha só vale ao aplicar, e fechar
|
|
242
|
+
* de outro jeito descarta. Sem ele, escolher já grava. */
|
|
243
|
+
withApplyButton?: boolean
|
|
244
|
+
/** Como o valor aparece no gatilho. */
|
|
245
|
+
valueFormatter?: (date: Date) => string
|
|
246
|
+
/** "dropdown" troca o título do mês por seletores de mês e ano. */
|
|
247
|
+
captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years"
|
|
248
|
+
size?: "sm" | "default"
|
|
249
|
+
disabled?: boolean
|
|
250
|
+
/** Classes do gatilho. */
|
|
251
|
+
className?: string
|
|
252
|
+
/** Classes do invólucro, que só existe com `clearable`. */
|
|
253
|
+
wrapperClassName?: string
|
|
254
|
+
align?: "start" | "center" | "end"
|
|
255
|
+
onOpenChange?: (open: boolean) => void
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export {
|
|
259
|
+
parseDateValue,
|
|
260
|
+
toDateValue,
|
|
261
|
+
toDateTimeValue,
|
|
262
|
+
formatDate,
|
|
263
|
+
formatDateTime,
|
|
264
|
+
clampDate,
|
|
265
|
+
outOfRange,
|
|
266
|
+
inRange,
|
|
267
|
+
PickerTrigger,
|
|
268
|
+
ClearButton,
|
|
269
|
+
PresetList,
|
|
270
|
+
PickerFooter,
|
|
271
|
+
PickerHint,
|
|
272
|
+
type FieldControlProps,
|
|
273
|
+
type PickerBaseProps,
|
|
274
|
+
}
|
|
@@ -23,6 +23,8 @@ function PopoverContent({
|
|
|
23
23
|
className,
|
|
24
24
|
align = "start",
|
|
25
25
|
sideOffset = 4,
|
|
26
|
+
onWheel,
|
|
27
|
+
onTouchMove,
|
|
26
28
|
...props
|
|
27
29
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
28
30
|
return (
|
|
@@ -31,6 +33,14 @@ function PopoverContent({
|
|
|
31
33
|
data-slot="popover-content"
|
|
32
34
|
align={align}
|
|
33
35
|
sideOffset={sideOffset}
|
|
36
|
+
// Dentro de um Dialog modal, o bloqueio de rolagem dele (react-remove-
|
|
37
|
+
// scroll) escuta a roda no document e cancela tudo o que está fora do
|
|
38
|
+
// Dialog — e o popover está num portal, fora. Sem isto, as listas do
|
|
39
|
+
// Combobox e do MultiSelect não rolam num Dialog. Parar a propagação
|
|
40
|
+
// aqui não deixa o evento chegar ao document; as listas usam
|
|
41
|
+
// overscroll-contain para a rolagem não vazar para a página.
|
|
42
|
+
onWheel={e => { e.stopPropagation(); onWheel?.(e) }}
|
|
43
|
+
onTouchMove={e => { e.stopPropagation(); onTouchMove?.(e) }}
|
|
34
44
|
className={cn(
|
|
35
45
|
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-lg bg-popover p-3 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
36
46
|
className
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils.js"
|
|
6
|
+
import { Input, type InputProps } from "./input.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hora, como o TimeInput do Mantine: o <input type="time"> do navegador, no
|
|
10
|
+
* visual do Input. O valor é "HH:mm" (ou "HH:mm:ss" com `withSeconds`).
|
|
11
|
+
*
|
|
12
|
+
* <Field label="Horário"><TimeInput value={hora} onChange={e => setHora(e.target.value)} /></Field>
|
|
13
|
+
*
|
|
14
|
+
* O formato de 12 ou 24 horas segue o sistema do usuário — no Brasil, 24 horas.
|
|
15
|
+
* O relógio à direita é o do navegador (abre a lista de horas no Chrome).
|
|
16
|
+
*/
|
|
17
|
+
function TimeInput({
|
|
18
|
+
withSeconds = false,
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}: Omit<InputProps, "type"> & {
|
|
22
|
+
withSeconds?: boolean
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<Input
|
|
26
|
+
type="time"
|
|
27
|
+
step={withSeconds ? 1 : 60}
|
|
28
|
+
className={cn("tabular-nums", className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { TimeInput }
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
/** "AAAA-MM-DD" → Date local à meia-noite. Valor vazio ou inválido → undefined. */
|
|
3
|
-
declare function parseDateValue(value: string | null | undefined): Date | undefined;
|
|
4
|
-
/** Date → "AAAA-MM-DD" no dia local. */
|
|
5
|
-
declare function toDateValue(date: Date): string;
|
|
6
|
-
type FieldProps = {
|
|
7
|
-
id?: string;
|
|
8
|
-
"aria-label"?: string;
|
|
9
|
-
"aria-describedby"?: string;
|
|
10
|
-
"aria-invalid"?: React.AriaAttributes["aria-invalid"];
|
|
11
|
-
};
|
|
12
|
-
type CommonProps = FieldProps & {
|
|
13
|
-
placeholder?: string;
|
|
14
|
-
/** Primeiro dia que pode ser escolhido ("AAAA-MM-DD"). */
|
|
15
|
-
minDate?: string;
|
|
16
|
-
/** Último dia que pode ser escolhido ("AAAA-MM-DD"). */
|
|
17
|
-
maxDate?: string;
|
|
18
|
-
/** Um X no gatilho limpa o valor. */
|
|
19
|
-
clearable?: boolean;
|
|
20
|
-
/** Como a data aparece no gatilho. Padrão: dd/mm/aaaa. */
|
|
21
|
-
formatValue?: (date: Date) => string;
|
|
22
|
-
/** "dropdown" troca o título do mês por seletores de mês e ano. */
|
|
23
|
-
captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years";
|
|
24
|
-
size?: "sm" | "default";
|
|
25
|
-
disabled?: boolean;
|
|
26
|
-
/** Classes do gatilho. */
|
|
27
|
-
className?: string;
|
|
28
|
-
/** Classes do invólucro, que só existe com `clearable`. */
|
|
29
|
-
wrapperClassName?: string;
|
|
30
|
-
align?: "start" | "center" | "end";
|
|
31
|
-
onOpenChange?: (open: boolean) => void;
|
|
32
|
-
};
|
|
33
|
-
export type DatePreset = {
|
|
34
|
-
label: string;
|
|
35
|
-
value: string;
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Escolha de uma data num calendário, como o DatePickerInput do Mantine. Base:
|
|
39
|
-
* o DatePicker do Tremor, no visual do kit (gatilho igual ao do Select).
|
|
40
|
-
*
|
|
41
|
-
* <Field label="Prazo">
|
|
42
|
-
* <DatePicker value={prazo} onValueChange={setPrazo} minDate={inicio} clearable />
|
|
43
|
-
* </Field>
|
|
44
|
-
*
|
|
45
|
-
* O valor é "AAAA-MM-DD" ou `null` (ver `parseDateValue`/`toDateValue`).
|
|
46
|
-
* Escolher o dia já fecha. `presets` põe atalhos ao lado ("Hoje", "Fim do
|
|
47
|
-
* mês"…); os que caem fora de `minDate`/`maxDate` ficam desabilitados.
|
|
48
|
-
*
|
|
49
|
-
* Teclado: o calendário abre com o foco no dia escolhido (ou hoje); setas
|
|
50
|
-
* andam pelos dias, PageUp/PageDown pelos meses, Enter escolhe, Esc fecha.
|
|
51
|
-
*/
|
|
52
|
-
declare function DatePicker({ value, onValueChange, placeholder, minDate, maxDate, clearable, presets, formatValue, captionLayout, size, disabled, className, wrapperClassName, align, onOpenChange, ...props }: CommonProps & {
|
|
53
|
-
value: string | null;
|
|
54
|
-
onValueChange: (value: string | null) => void;
|
|
55
|
-
presets?: DatePreset[];
|
|
56
|
-
}): React.JSX.Element;
|
|
57
|
-
export type DateRangeValue = [string | null, string | null];
|
|
58
|
-
export type DateRangePreset = {
|
|
59
|
-
label: string;
|
|
60
|
-
value: [string, string];
|
|
61
|
-
};
|
|
62
|
-
/**
|
|
63
|
-
* Escolha de um período (início e fim) num calendário de dois meses — um só no
|
|
64
|
-
* celular. Base: o DateRangePicker do Tremor, no visual do kit.
|
|
65
|
-
*
|
|
66
|
-
* <DateRangePicker value={[inicio, fim]} onValueChange={([i, f]) => { … }} />
|
|
67
|
-
*
|
|
68
|
-
* O primeiro clique marca o início e o segundo, o fim; aí fecha e grava. Se o
|
|
69
|
-
* segundo dia vier antes do primeiro, os dois trocam de lugar. Fechar no meio
|
|
70
|
-
* descarta o período começado e mantém o anterior.
|
|
71
|
-
*/
|
|
72
|
-
declare function DateRangePicker({ value, onValueChange, placeholder, minDate, maxDate, clearable, presets, formatValue, captionLayout, size, disabled, className, wrapperClassName, align, onOpenChange, ...props }: CommonProps & {
|
|
73
|
-
value: DateRangeValue;
|
|
74
|
-
onValueChange: (value: DateRangeValue) => void;
|
|
75
|
-
presets?: DateRangePreset[];
|
|
76
|
-
}): React.JSX.Element;
|
|
77
|
-
export { DatePicker, DateRangePicker, parseDateValue, toDateValue };
|
|
78
|
-
//# sourceMappingURL=date-picker.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"date-picker.d.ts","sourceRoot":"","sources":["../../src/components/date-picker.tsx"],"names":[],"mappings":"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"}
|