@motor-hero/ui-kit 0.32.1 → 0.34.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/dist/components/date-picker.d.ts +21 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/pagination.d.ts +9 -3
- package/dist/components/pagination.d.ts.map +1 -1
- package/dist/index.cjs +475 -159
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +484 -170
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/components/date-picker.tsx +348 -0
- package/src/components/pagination.tsx +67 -20
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motor-hero/ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Componentes React reutilizáveis para o ecossistema MotorHero — shadcn/ui + Tailwind CSS v4",
|
|
5
5
|
"author": "MotorHero",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -60,9 +60,12 @@
|
|
|
60
60
|
"@radix-ui/react-dropdown-menu": "^2.1.0",
|
|
61
61
|
"@radix-ui/react-popover": "^1.1.16",
|
|
62
62
|
"@radix-ui/react-slot": "^1.1.0",
|
|
63
|
+
"@react-input/mask": "^2.0.4",
|
|
63
64
|
"class-variance-authority": "^0.7.0",
|
|
64
65
|
"cmdk": "^1.1.1",
|
|
66
|
+
"date-fns": "^4.4.0",
|
|
65
67
|
"react-colorful": "^5.7.0",
|
|
68
|
+
"react-day-picker": "^10.0.1",
|
|
66
69
|
"sonner": "^2.0.7",
|
|
67
70
|
"vaul": "^1.1.2"
|
|
68
71
|
},
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
2
|
+
import { InputMask } from "@react-input/mask"
|
|
3
|
+
import { format, isValid, parse } from "date-fns"
|
|
4
|
+
import { ptBR } from "date-fns/locale"
|
|
5
|
+
import { CalendarIcon, ChevronLeft, ChevronRight, Clock } from "lucide-react"
|
|
6
|
+
import * as React from "react"
|
|
7
|
+
import { DayPicker, type Matcher } from "react-day-picker"
|
|
8
|
+
import { Drawer as DrawerPrimitive } from "vaul"
|
|
9
|
+
import { useIsDesktop } from "../hooks/use-is-desktop"
|
|
10
|
+
import { cn } from "../lib/utils"
|
|
11
|
+
|
|
12
|
+
const DATE_MASK = "__/__/____"
|
|
13
|
+
const TIME_MASK = "__:__"
|
|
14
|
+
const MASK_REPLACEMENT = { _: /\d/ }
|
|
15
|
+
|
|
16
|
+
const DATE_FORMAT = "dd/MM/yyyy"
|
|
17
|
+
const TIME_FORMAT = "HH:mm"
|
|
18
|
+
const DATE_VALUE_FORMAT = "yyyy-MM-dd"
|
|
19
|
+
|
|
20
|
+
export interface DatePickerProps {
|
|
21
|
+
/** `DatePicker`: data pura `"yyyy-MM-dd"`, sem conversão de fuso. `DateTimePicker`:
|
|
22
|
+
* ISO UTC completo (`...Z`). `""`/`undefined` = sem valor. */
|
|
23
|
+
value?: string
|
|
24
|
+
onChange: (value: string) => void
|
|
25
|
+
placeholder?: string
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
/** `"yyyy-MM-dd"`, inclusive. */
|
|
28
|
+
minDate?: string
|
|
29
|
+
/** `"yyyy-MM-dd"`, inclusive. */
|
|
30
|
+
maxDate?: string
|
|
31
|
+
disabledDates?: (date: Date) => boolean
|
|
32
|
+
id?: string
|
|
33
|
+
className?: string
|
|
34
|
+
"aria-invalid"?: boolean | "true" | "false"
|
|
35
|
+
"aria-describedby"?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface DatePickerImplProps extends DatePickerProps {
|
|
39
|
+
withTime?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pad(n: number): string {
|
|
43
|
+
return String(n).padStart(2, "0")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Parseia `text` num formato estrito, rejeitando rollover (ex. 31/02 -> 03/03,
|
|
47
|
+
* 25:99 -> next day) via round-trip: só é válido se formatar de volta pro
|
|
48
|
+
* mesmo texto exato digitado. */
|
|
49
|
+
function parseStrict(text: string, fmt: string): Date | null {
|
|
50
|
+
const parsed = parse(text, fmt, new Date())
|
|
51
|
+
if (!isValid(parsed)) return null
|
|
52
|
+
return format(parsed, fmt) === text ? parsed : null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function dateOnlyToDate(value: string | undefined): Date | undefined {
|
|
56
|
+
if (!value) return undefined
|
|
57
|
+
const parsed = parse(value, DATE_VALUE_FORMAT, new Date())
|
|
58
|
+
return isValid(parsed) ? parsed : undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function dateTimeToDate(value: string | undefined): Date | undefined {
|
|
62
|
+
if (!value) return undefined
|
|
63
|
+
const parsed = new Date(value)
|
|
64
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function displayFor(value: string | undefined, withTime: boolean): string {
|
|
68
|
+
const date = withTime ? dateTimeToDate(value) : dateOnlyToDate(value)
|
|
69
|
+
if (!date) return ""
|
|
70
|
+
const datePart = format(date, DATE_FORMAT)
|
|
71
|
+
return withTime ? `${datePart} ${pad(date.getHours())}:${pad(date.getMinutes())}` : datePart
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toMatchers(
|
|
75
|
+
minDate: string | undefined,
|
|
76
|
+
maxDate: string | undefined,
|
|
77
|
+
disabledDates: ((date: Date) => boolean) | undefined,
|
|
78
|
+
): Matcher[] {
|
|
79
|
+
const matchers: Matcher[] = []
|
|
80
|
+
const min = dateOnlyToDate(minDate)
|
|
81
|
+
const max = dateOnlyToDate(maxDate)
|
|
82
|
+
if (min) matchers.push({ before: min })
|
|
83
|
+
if (max) matchers.push({ after: max })
|
|
84
|
+
if (disabledDates) matchers.push(disabledDates)
|
|
85
|
+
return matchers
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const triggerInputClass =
|
|
89
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 pr-10 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive"
|
|
90
|
+
|
|
91
|
+
const fieldInputClass =
|
|
92
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
93
|
+
|
|
94
|
+
const calendarClassNames = {
|
|
95
|
+
months: "flex flex-col",
|
|
96
|
+
month: "flex flex-col gap-4",
|
|
97
|
+
month_caption: "flex justify-center pt-1 relative items-center h-7",
|
|
98
|
+
caption_label: "text-sm font-medium",
|
|
99
|
+
nav: "flex items-center gap-1 absolute inset-x-0 top-0 justify-between px-1",
|
|
100
|
+
button_previous:
|
|
101
|
+
"inline-flex size-7 items-center justify-center rounded-md border border-input bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
102
|
+
button_next:
|
|
103
|
+
"inline-flex size-7 items-center justify-center rounded-md border border-input bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
104
|
+
month_grid: "w-full border-collapse space-y-1",
|
|
105
|
+
weekdays: "flex",
|
|
106
|
+
weekday: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
|
107
|
+
week: "flex w-full mt-2",
|
|
108
|
+
day: "relative p-0 text-center text-sm focus-within:relative focus-within:z-20",
|
|
109
|
+
day_button:
|
|
110
|
+
"inline-flex size-8 items-center justify-center rounded-md p-0 font-normal hover:bg-accent hover:text-accent-foreground aria-selected:opacity-100",
|
|
111
|
+
selected:
|
|
112
|
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground rounded-md [&>button]:bg-primary [&>button]:text-primary-foreground",
|
|
113
|
+
today: "bg-accent text-accent-foreground rounded-md",
|
|
114
|
+
outside: "text-muted-foreground opacity-50",
|
|
115
|
+
disabled: "text-muted-foreground opacity-50 pointer-events-none",
|
|
116
|
+
hidden: "invisible",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const calendarComponents = {
|
|
120
|
+
Chevron: ({ orientation }: { orientation?: string }) =>
|
|
121
|
+
orientation === "left" ? (
|
|
122
|
+
<ChevronLeft className="size-4" />
|
|
123
|
+
) : (
|
|
124
|
+
<ChevronRight className="size-4" />
|
|
125
|
+
),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* DatePicker / DateTimePicker — calendário + campo digitável com máscara
|
|
130
|
+
* (nunca o `<input type="date"/time">` nativo). Desktop: Popover não-modal;
|
|
131
|
+
* mobile: Drawer (bottom sheet) — mesmo padrão de `Combobox`/`ColorPicker`.
|
|
132
|
+
*
|
|
133
|
+
* `DatePicker` fecha ao escolher o dia (não tem hora pra confirmar).
|
|
134
|
+
* `DateTimePicker` exige "Aplicar" — calendário e campo de horário só viram
|
|
135
|
+
* `value` quando os dois estão preenchidos.
|
|
136
|
+
*/
|
|
137
|
+
function DatePickerImpl({
|
|
138
|
+
value,
|
|
139
|
+
onChange,
|
|
140
|
+
placeholder,
|
|
141
|
+
disabled,
|
|
142
|
+
minDate,
|
|
143
|
+
maxDate,
|
|
144
|
+
disabledDates,
|
|
145
|
+
withTime = false,
|
|
146
|
+
id,
|
|
147
|
+
className,
|
|
148
|
+
"aria-invalid": ariaInvalid,
|
|
149
|
+
"aria-describedby": ariaDescribedby,
|
|
150
|
+
}: DatePickerImplProps) {
|
|
151
|
+
const isDesktop = useIsDesktop()
|
|
152
|
+
const triggerRef = React.useRef<HTMLDivElement>(null)
|
|
153
|
+
const [open, setOpenRaw] = React.useState(false)
|
|
154
|
+
const [focused, setFocused] = React.useState(false)
|
|
155
|
+
const [editText, setEditText] = React.useState("")
|
|
156
|
+
const [pendingDate, setPendingDate] = React.useState<Date | undefined>()
|
|
157
|
+
const [pendingTime, setPendingTime] = React.useState("")
|
|
158
|
+
|
|
159
|
+
const committedDate = withTime ? dateTimeToDate(value) : dateOnlyToDate(value)
|
|
160
|
+
const displayValue = displayFor(value, withTime)
|
|
161
|
+
const matchers = toMatchers(minDate, maxDate, disabledDates)
|
|
162
|
+
|
|
163
|
+
const setOpen = (next: boolean) => {
|
|
164
|
+
setOpenRaw(next)
|
|
165
|
+
if (next) {
|
|
166
|
+
setEditText(displayValue)
|
|
167
|
+
setPendingDate(committedDate)
|
|
168
|
+
setPendingTime(
|
|
169
|
+
committedDate ? `${pad(committedDate.getHours())}:${pad(committedDate.getMinutes())}` : "",
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const commitDateOnly = (date: Date) => {
|
|
175
|
+
onChange(format(date, DATE_VALUE_FORMAT))
|
|
176
|
+
setOpen(false)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const commitDateTime = (date: Date, timeText: string) => {
|
|
180
|
+
const time = parseStrict(timeText, TIME_FORMAT)
|
|
181
|
+
if (!time) return
|
|
182
|
+
const combined = new Date(date)
|
|
183
|
+
combined.setHours(time.getHours(), time.getMinutes(), 0, 0)
|
|
184
|
+
onChange(combined.toISOString())
|
|
185
|
+
setOpen(false)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const handleCalendarSelect = (date: Date) => {
|
|
189
|
+
if (!withTime) {
|
|
190
|
+
commitDateOnly(date)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
setPendingDate(date)
|
|
194
|
+
setEditText(`${format(date, DATE_FORMAT)} ${pendingTime}`.trim())
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const handleTriggerChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
198
|
+
const text = event.target.value
|
|
199
|
+
setEditText(text)
|
|
200
|
+
if (!withTime) {
|
|
201
|
+
const parsed = parseStrict(text, DATE_FORMAT)
|
|
202
|
+
if (parsed) commitDateOnly(parsed)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
const [datePart, timePart] = [text.slice(0, 10), text.slice(11)]
|
|
206
|
+
const parsedDate = parseStrict(datePart, DATE_FORMAT)
|
|
207
|
+
if (parsedDate) {
|
|
208
|
+
setPendingDate(parsedDate)
|
|
209
|
+
if (timePart) setPendingTime(timePart)
|
|
210
|
+
}
|
|
211
|
+
if (parsedDate && timePart && parseStrict(timePart, TIME_FORMAT)) {
|
|
212
|
+
commitDateTime(parsedDate, timePart)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const handleTimeFieldChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
217
|
+
setPendingTime(event.target.value)
|
|
218
|
+
if (pendingDate) {
|
|
219
|
+
setEditText(`${format(pendingDate, DATE_FORMAT)} ${event.target.value}`.trim())
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const canApply = withTime && !!pendingDate && !!parseStrict(pendingTime, TIME_FORMAT)
|
|
224
|
+
|
|
225
|
+
const trigger = (
|
|
226
|
+
<div ref={triggerRef} className="relative">
|
|
227
|
+
<InputMask
|
|
228
|
+
id={id}
|
|
229
|
+
mask={withTime ? `${DATE_MASK} ${TIME_MASK}` : DATE_MASK}
|
|
230
|
+
replacement={MASK_REPLACEMENT}
|
|
231
|
+
value={focused ? editText : displayValue}
|
|
232
|
+
onChange={handleTriggerChange}
|
|
233
|
+
onFocus={() => {
|
|
234
|
+
setFocused(true)
|
|
235
|
+
setOpen(true)
|
|
236
|
+
}}
|
|
237
|
+
onBlur={() => setFocused(false)}
|
|
238
|
+
placeholder={placeholder ?? (withTime ? "dd/mm/aaaa hh:mm" : "dd/mm/aaaa")}
|
|
239
|
+
disabled={disabled}
|
|
240
|
+
aria-invalid={ariaInvalid}
|
|
241
|
+
aria-describedby={ariaDescribedby}
|
|
242
|
+
className={cn(triggerInputClass, className)}
|
|
243
|
+
/>
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
disabled={disabled}
|
|
247
|
+
aria-label={withTime ? "Abrir calendário e horário" : "Abrir calendário"}
|
|
248
|
+
aria-haspopup="dialog"
|
|
249
|
+
aria-expanded={open}
|
|
250
|
+
onClick={() => setOpen(!open)}
|
|
251
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
|
|
252
|
+
>
|
|
253
|
+
{withTime ? <Clock className="h-4 w-4" /> : <CalendarIcon className="h-4 w-4" />}
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const content = (
|
|
259
|
+
<div className={withTime ? "p-3" : ""}>
|
|
260
|
+
<DayPicker
|
|
261
|
+
mode="single"
|
|
262
|
+
locale={ptBR}
|
|
263
|
+
selected={withTime ? pendingDate : committedDate}
|
|
264
|
+
defaultMonth={(withTime ? pendingDate : committedDate) ?? new Date()}
|
|
265
|
+
onSelect={(date) => date && handleCalendarSelect(date)}
|
|
266
|
+
disabled={matchers}
|
|
267
|
+
className={withTime ? "p-0" : "p-3"}
|
|
268
|
+
classNames={calendarClassNames}
|
|
269
|
+
components={calendarComponents}
|
|
270
|
+
/>
|
|
271
|
+
{withTime && (
|
|
272
|
+
<>
|
|
273
|
+
<div className="mt-2 flex items-center gap-2 border-t pt-3">
|
|
274
|
+
<span className="text-sm text-muted-foreground">Horário</span>
|
|
275
|
+
<InputMask
|
|
276
|
+
mask={TIME_MASK}
|
|
277
|
+
replacement={MASK_REPLACEMENT}
|
|
278
|
+
value={pendingTime}
|
|
279
|
+
onChange={handleTimeFieldChange}
|
|
280
|
+
placeholder="hh:mm"
|
|
281
|
+
aria-label="Horário"
|
|
282
|
+
className={cn(fieldInputClass, "flex-1")}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
disabled={!canApply}
|
|
288
|
+
onClick={() => pendingDate && commitDateTime(pendingDate, pendingTime)}
|
|
289
|
+
className="mt-3 flex h-9 w-full items-center justify-center rounded-md bg-primary text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
|
|
290
|
+
>
|
|
291
|
+
Aplicar
|
|
292
|
+
</button>
|
|
293
|
+
</>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if (isDesktop) {
|
|
299
|
+
return (
|
|
300
|
+
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
301
|
+
<PopoverPrimitive.Anchor asChild>{trigger}</PopoverPrimitive.Anchor>
|
|
302
|
+
<PopoverPrimitive.Portal>
|
|
303
|
+
<PopoverPrimitive.Content
|
|
304
|
+
align="start"
|
|
305
|
+
sideOffset={4}
|
|
306
|
+
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
307
|
+
onInteractOutside={(event) => {
|
|
308
|
+
if (triggerRef.current?.contains(event.target as Node)) event.preventDefault()
|
|
309
|
+
}}
|
|
310
|
+
className="pointer-events-auto z-50 w-auto overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md"
|
|
311
|
+
>
|
|
312
|
+
{content}
|
|
313
|
+
</PopoverPrimitive.Content>
|
|
314
|
+
</PopoverPrimitive.Portal>
|
|
315
|
+
</PopoverPrimitive.Root>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<DrawerPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
321
|
+
{trigger}
|
|
322
|
+
<DrawerPrimitive.Portal>
|
|
323
|
+
<DrawerPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80" />
|
|
324
|
+
<DrawerPrimitive.Content
|
|
325
|
+
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
326
|
+
onInteractOutside={(event) => {
|
|
327
|
+
if (triggerRef.current?.contains(event.target as Node)) event.preventDefault()
|
|
328
|
+
}}
|
|
329
|
+
className="fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[85vh] flex-col rounded-t-2xl border bg-popover text-popover-foreground outline-none"
|
|
330
|
+
>
|
|
331
|
+
<DrawerPrimitive.Title className="sr-only">
|
|
332
|
+
{withTime ? "Selecione a data e o horário" : "Selecione a data"}
|
|
333
|
+
</DrawerPrimitive.Title>
|
|
334
|
+
<div className="mx-auto my-3 h-1.5 w-12 shrink-0 rounded-full bg-muted" />
|
|
335
|
+
<div className="flex justify-center overflow-y-auto pb-4">{content}</div>
|
|
336
|
+
</DrawerPrimitive.Content>
|
|
337
|
+
</DrawerPrimitive.Portal>
|
|
338
|
+
</DrawerPrimitive.Root>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function DatePicker(props: DatePickerProps) {
|
|
343
|
+
return <DatePickerImpl {...props} />
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function DateTimePicker(props: DatePickerProps) {
|
|
347
|
+
return <DatePickerImpl {...props} withTime />
|
|
348
|
+
}
|
|
@@ -1,31 +1,78 @@
|
|
|
1
|
-
interface PaginationProps {
|
|
1
|
+
export interface PaginationProps {
|
|
2
2
|
page: number
|
|
3
3
|
onPageChange: (page: number) => void
|
|
4
4
|
hasNextPage: boolean
|
|
5
5
|
hasPreviousPage: boolean
|
|
6
|
+
/** Total de páginas. Quando informado, o rótulo vira "Página {page} de {totalPages}". */
|
|
7
|
+
totalPages?: number
|
|
8
|
+
/** Exibe o seletor de itens por página. Use com `pageSize`, `onPageSizeChange` e `pageSizeOptions`. */
|
|
9
|
+
showPageSizeSelector?: boolean
|
|
10
|
+
pageSize?: number
|
|
11
|
+
onPageSizeChange?: (size: number) => void
|
|
12
|
+
pageSizeOptions?: number[]
|
|
6
13
|
className?: string
|
|
7
14
|
}
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
const navButtonClass =
|
|
17
|
+
"inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer disabled:pointer-events-none disabled:opacity-50"
|
|
18
|
+
|
|
19
|
+
export function Pagination({
|
|
20
|
+
page,
|
|
21
|
+
onPageChange,
|
|
22
|
+
hasNextPage,
|
|
23
|
+
hasPreviousPage,
|
|
24
|
+
totalPages,
|
|
25
|
+
showPageSizeSelector = false,
|
|
26
|
+
pageSize,
|
|
27
|
+
onPageSizeChange,
|
|
28
|
+
pageSizeOptions,
|
|
29
|
+
className,
|
|
30
|
+
}: PaginationProps) {
|
|
10
31
|
return (
|
|
11
|
-
<div
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
<div
|
|
33
|
+
className={`flex items-center ${showPageSizeSelector ? "justify-between" : "justify-end"} gap-4 ${className ?? ""}`}
|
|
34
|
+
>
|
|
35
|
+
{showPageSizeSelector && (
|
|
36
|
+
<div className="flex items-center gap-2">
|
|
37
|
+
<label htmlFor="pagination-page-size" className="text-sm text-muted-foreground">
|
|
38
|
+
Itens por página
|
|
39
|
+
</label>
|
|
40
|
+
<select
|
|
41
|
+
id="pagination-page-size"
|
|
42
|
+
value={pageSize}
|
|
43
|
+
onChange={(e) => onPageSizeChange?.(Number(e.target.value))}
|
|
44
|
+
className="h-8 rounded-md border border-input bg-background px-2 text-xs font-medium shadow-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-pointer"
|
|
45
|
+
>
|
|
46
|
+
{(pageSizeOptions ?? []).map((size) => (
|
|
47
|
+
<option key={size} value={size}>
|
|
48
|
+
{size}
|
|
49
|
+
</option>
|
|
50
|
+
))}
|
|
51
|
+
</select>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
<div className="flex items-center gap-4">
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={() => onPageChange(page - 1)}
|
|
58
|
+
disabled={!hasPreviousPage}
|
|
59
|
+
className={navButtonClass}
|
|
60
|
+
>
|
|
61
|
+
Anterior
|
|
62
|
+
</button>
|
|
63
|
+
<span className="text-sm text-muted-foreground">
|
|
64
|
+
Página {page}
|
|
65
|
+
{totalPages ? ` de ${totalPages}` : ""}
|
|
66
|
+
</span>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={() => onPageChange(page + 1)}
|
|
70
|
+
disabled={!hasNextPage}
|
|
71
|
+
className={navButtonClass}
|
|
72
|
+
>
|
|
73
|
+
Próximo
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
29
76
|
</div>
|
|
30
77
|
)
|
|
31
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export { FormDialogLayout, FormDialog } from "./components/form-dialog"
|
|
|
25
25
|
export type { FormDialogProps, FormDialogSize } from "./components/form-dialog"
|
|
26
26
|
export { AuthCard } from "./components/auth-card"
|
|
27
27
|
export { Pagination } from "./components/pagination"
|
|
28
|
+
export type { PaginationProps } from "./components/pagination"
|
|
28
29
|
export { TableSkeleton } from "./components/table-skeleton"
|
|
29
30
|
export { SearchInput } from "./components/search-input"
|
|
30
31
|
export { PasswordInput } from "./components/password-input"
|
|
@@ -34,6 +35,8 @@ export { MultiCombobox } from "./components/multi-combobox"
|
|
|
34
35
|
export type { MultiComboboxProps } from "./components/multi-combobox"
|
|
35
36
|
export { ColorPicker } from "./components/color-picker"
|
|
36
37
|
export type { ColorPickerProps } from "./components/color-picker"
|
|
38
|
+
export { DatePicker, DateTimePicker } from "./components/date-picker"
|
|
39
|
+
export type { DatePickerProps } from "./components/date-picker"
|
|
37
40
|
export { StatCard } from "./components/stat-card"
|
|
38
41
|
export { DataTableWrapper } from "./components/data-table-wrapper"
|
|
39
42
|
export { MobileCardList } from "./components/mobile-card-list"
|