@strategicos/ui 0.8.0 → 0.9.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 (33) 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/time-input.d.ts +16 -0
  20. package/dist/components/time-input.d.ts.map +1 -0
  21. package/dist/components/time-input.js +18 -0
  22. package/dist/components/time-input.js.map +1 -0
  23. package/package.json +1 -1
  24. package/src/components/date-input.tsx +287 -0
  25. package/src/components/date-picker-input.tsx +287 -0
  26. package/src/components/date-time-picker.tsx +152 -0
  27. package/src/components/dates.tsx +274 -0
  28. package/src/components/time-input.tsx +34 -0
  29. package/dist/components/date-picker.d.ts +0 -78
  30. package/dist/components/date-picker.d.ts.map +0 -1
  31. package/dist/components/date-picker.js +0 -142
  32. package/dist/components/date-picker.js.map +0 -1
  33. package/src/components/date-picker.tsx +0 -414
@@ -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 }
@@ -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 }