@open-mercato/ui 0.6.4-develop.4178.1.aad9ddaa95 → 0.6.4-develop.4210.1.d412061cfe
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +14 -0
- package/dist/backend/DataTable.js +28 -62
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/primitives/activity-feed.js +196 -0
- package/dist/primitives/activity-feed.js.map +7 -0
- package/dist/primitives/avatar.js +140 -14
- package/dist/primitives/avatar.js.map +3 -3
- package/dist/primitives/badge.js +94 -5
- package/dist/primitives/badge.js.map +2 -2
- package/dist/primitives/button-group.js +52 -0
- package/dist/primitives/button-group.js.map +7 -0
- package/dist/primitives/color-picker.js +520 -0
- package/dist/primitives/color-picker.js.map +7 -0
- package/dist/primitives/command-menu.js +355 -0
- package/dist/primitives/command-menu.js.map +7 -0
- package/dist/primitives/dialog.js +210 -35
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/drawer.js +219 -0
- package/dist/primitives/drawer.js.map +7 -0
- package/dist/primitives/notification-feed.js +213 -0
- package/dist/primitives/notification-feed.js.map +7 -0
- package/dist/primitives/notification.js +23 -2
- package/dist/primitives/notification.js.map +2 -2
- package/dist/primitives/pagination.js +245 -0
- package/dist/primitives/pagination.js.map +7 -0
- package/dist/primitives/progress.js +178 -5
- package/dist/primitives/progress.js.map +2 -2
- package/dist/primitives/rating.js +174 -0
- package/dist/primitives/rating.js.map +7 -0
- package/dist/primitives/scroll-area.js +82 -0
- package/dist/primitives/scroll-area.js.map +7 -0
- package/dist/primitives/segmented-control.js +101 -0
- package/dist/primitives/segmented-control.js.map +7 -0
- package/dist/primitives/separator.js +103 -4
- package/dist/primitives/separator.js.map +2 -2
- package/dist/primitives/slider.js +115 -0
- package/dist/primitives/slider.js.map +7 -0
- package/dist/primitives/step-indicator.js +187 -0
- package/dist/primitives/step-indicator.js.map +7 -0
- package/dist/primitives/table.js +102 -10
- package/dist/primitives/table.js.map +2 -2
- package/dist/primitives/tabs.js +122 -14
- package/dist/primitives/tabs.js.map +2 -2
- package/dist/primitives/time-picker.js +9 -11
- package/dist/primitives/time-picker.js.map +2 -2
- package/package.json +8 -4
- package/src/backend/DataTable.tsx +43 -60
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/primitives/__tests__/activity-feed.test.tsx +216 -0
- package/src/primitives/__tests__/avatar.test.tsx +131 -0
- package/src/primitives/__tests__/badge.test.tsx +189 -0
- package/src/primitives/__tests__/button-group.test.tsx +117 -0
- package/src/primitives/__tests__/color-picker.test.tsx +589 -0
- package/src/primitives/__tests__/command-menu.test.tsx +231 -0
- package/src/primitives/__tests__/dialog.test.tsx +290 -0
- package/src/primitives/__tests__/drawer.test.tsx +264 -0
- package/src/primitives/__tests__/notification-feed.test.tsx +303 -0
- package/src/primitives/__tests__/notification.test.tsx +102 -2
- package/src/primitives/__tests__/pagination.test.tsx +268 -0
- package/src/primitives/__tests__/progress.test.tsx +222 -0
- package/src/primitives/__tests__/rating.test.tsx +164 -0
- package/src/primitives/__tests__/scroll-area.test.tsx +109 -0
- package/src/primitives/__tests__/segmented-control.test.tsx +129 -0
- package/src/primitives/__tests__/separator.test.tsx +143 -0
- package/src/primitives/__tests__/slider.test.tsx +86 -0
- package/src/primitives/__tests__/step-indicator.test.tsx +205 -0
- package/src/primitives/__tests__/table.test.tsx +199 -0
- package/src/primitives/__tests__/tabs.test.tsx +139 -0
- package/src/primitives/activity-feed.tsx +326 -0
- package/src/primitives/avatar.tsx +225 -22
- package/src/primitives/badge.tsx +141 -6
- package/src/primitives/button-group.tsx +100 -0
- package/src/primitives/color-picker.tsx +693 -0
- package/src/primitives/command-menu.tsx +503 -0
- package/src/primitives/dialog.tsx +315 -51
- package/src/primitives/drawer.tsx +338 -0
- package/src/primitives/notification-feed.tsx +358 -0
- package/src/primitives/notification.tsx +42 -2
- package/src/primitives/pagination.tsx +376 -0
- package/src/primitives/progress.tsx +267 -6
- package/src/primitives/rating.tsx +245 -0
- package/src/primitives/scroll-area.tsx +169 -0
- package/src/primitives/segmented-control.tsx +162 -0
- package/src/primitives/separator.tsx +163 -3
- package/src/primitives/slider.tsx +160 -0
- package/src/primitives/step-indicator.tsx +287 -0
- package/src/primitives/table.tsx +142 -11
- package/src/primitives/tabs.tsx +174 -12
- package/src/primitives/time-picker.tsx +9 -11
|
@@ -60,13 +60,27 @@ export type NotificationProps = {
|
|
|
60
60
|
className?: string
|
|
61
61
|
/** Stable id used by the `NotificationStack` (or external trackers) to identify the card. Forwarded as `data-notification-id`. */
|
|
62
62
|
id?: string
|
|
63
|
+
/**
|
|
64
|
+
* Auto-dismiss the card after N milliseconds. When set, an internal
|
|
65
|
+
* timer fires `onDismiss` after the delay. Standard toast UX:
|
|
66
|
+
* 4000–6000 for success / info, 8000+ for warnings, no auto-dismiss
|
|
67
|
+
* for errors. Defaults to undefined (manual dismiss only).
|
|
68
|
+
*/
|
|
69
|
+
autoDismissMs?: number
|
|
70
|
+
/**
|
|
71
|
+
* Pause the auto-dismiss timer while the user is hovering the card.
|
|
72
|
+
* Defaults to `true` whenever `autoDismissMs` is set — gives users
|
|
73
|
+
* time to read/click without the card disappearing under their
|
|
74
|
+
* cursor. Pass `false` to keep the timer running unconditionally.
|
|
75
|
+
*/
|
|
76
|
+
pauseOnHover?: boolean
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
/**
|
|
66
80
|
* Card composition over the `Alert` primitive matching the Figma
|
|
67
81
|
* Notification reference (`170:1839` — Error/Light/Large). Renders a
|
|
68
82
|
* status icon (or custom `avatar`), a title + timestamp row, an
|
|
69
|
-
* `opacity-
|
|
83
|
+
* `opacity-70` description, and an optional row of action links. Use
|
|
70
84
|
* with `NotificationStack` for corner-floating manual-dismiss UX, or
|
|
71
85
|
* standalone inside a panel / dialog content area.
|
|
72
86
|
*
|
|
@@ -89,9 +103,30 @@ export const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
|
|
89
103
|
dismissAriaLabel,
|
|
90
104
|
className,
|
|
91
105
|
id,
|
|
106
|
+
autoDismissMs,
|
|
107
|
+
pauseOnHover,
|
|
92
108
|
},
|
|
93
109
|
ref,
|
|
94
110
|
) => {
|
|
111
|
+
const shouldPauseOnHover = pauseOnHover ?? autoDismissMs !== undefined
|
|
112
|
+
const [hovered, setHovered] = React.useState(false)
|
|
113
|
+
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
if (autoDismissMs === undefined || autoDismissMs <= 0) return
|
|
116
|
+
if (shouldPauseOnHover && hovered) return
|
|
117
|
+
const timer = window.setTimeout(() => {
|
|
118
|
+
onDismiss?.()
|
|
119
|
+
}, autoDismissMs)
|
|
120
|
+
return () => window.clearTimeout(timer)
|
|
121
|
+
}, [autoDismissMs, hovered, onDismiss, shouldPauseOnHover])
|
|
122
|
+
|
|
123
|
+
const handleMouseEnter = React.useCallback(() => {
|
|
124
|
+
if (shouldPauseOnHover) setHovered(true)
|
|
125
|
+
}, [shouldPauseOnHover])
|
|
126
|
+
const handleMouseLeave = React.useCallback(() => {
|
|
127
|
+
if (shouldPauseOnHover) setHovered(false)
|
|
128
|
+
}, [shouldPauseOnHover])
|
|
129
|
+
|
|
95
130
|
return (
|
|
96
131
|
<Alert
|
|
97
132
|
ref={ref}
|
|
@@ -105,6 +140,11 @@ export const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
|
|
105
140
|
className={className}
|
|
106
141
|
data-slot="notification"
|
|
107
142
|
data-notification-id={id}
|
|
143
|
+
data-auto-dismiss-paused={
|
|
144
|
+
autoDismissMs !== undefined && shouldPauseOnHover && hovered ? 'true' : undefined
|
|
145
|
+
}
|
|
146
|
+
onMouseEnter={handleMouseEnter}
|
|
147
|
+
onMouseLeave={handleMouseLeave}
|
|
108
148
|
>
|
|
109
149
|
{(title || timestamp) && (
|
|
110
150
|
<div className="flex items-start gap-2">
|
|
@@ -117,7 +157,7 @@ export const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
|
|
117
157
|
</div>
|
|
118
158
|
)}
|
|
119
159
|
{description ? (
|
|
120
|
-
<AlertDescription className={cn('opacity-
|
|
160
|
+
<AlertDescription className={cn('opacity-70', title && 'mt-1')} data-slot="notification-description">
|
|
121
161
|
{description}
|
|
122
162
|
</AlertDescription>
|
|
123
163
|
) : null}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
ChevronLeft,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
ChevronsLeft,
|
|
8
|
+
ChevronsRight,
|
|
9
|
+
} from 'lucide-react'
|
|
10
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
11
|
+
|
|
12
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
13
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
14
|
+
import {
|
|
15
|
+
CompactSelectTrigger,
|
|
16
|
+
Select,
|
|
17
|
+
SelectContent,
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from './compact-select'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Page navigation primitive per Figma `Pagination Group [1.1]` (DS Open
|
|
24
|
+
* Mercato componentSet `199985:4135`).
|
|
25
|
+
*
|
|
26
|
+
* Layout (Figma `Basic` variant):
|
|
27
|
+
*
|
|
28
|
+
* [Left] "Page 2 of 16"
|
|
29
|
+
* [Center] ⏮ ◀ [1][2]…[N-1][N] ▶ ⏭
|
|
30
|
+
* [Right] "7 / page" CompactSelect
|
|
31
|
+
*
|
|
32
|
+
* Figma's three boolean variant props are mapped 1:1 to React props:
|
|
33
|
+
*
|
|
34
|
+
* 🥇 First / Last → `showFirstLast` (default true)
|
|
35
|
+
* ⏭️ Next / Previous → `showPrevNext` (default true)
|
|
36
|
+
* 🧪 Advanced → `showInfo` + `showPageSize` (default both true)
|
|
37
|
+
*
|
|
38
|
+
* The numeric page list uses the same ellipsis algorithm Material /
|
|
39
|
+
* MUI / shadcn ship: `boundaryCount` pages at each end (default 1),
|
|
40
|
+
* `siblingCount` pages on either side of the current page (default 1),
|
|
41
|
+
* collapse the rest into `…` placeholders.
|
|
42
|
+
*
|
|
43
|
+
* ```tsx
|
|
44
|
+
* const [page, setPage] = React.useState(1)
|
|
45
|
+
* const [pageSize, setPageSize] = React.useState(25)
|
|
46
|
+
* <Pagination
|
|
47
|
+
* page={page}
|
|
48
|
+
* pageSize={pageSize}
|
|
49
|
+
* total={items.length}
|
|
50
|
+
* onPageChange={setPage}
|
|
51
|
+
* onPageSizeChange={setPageSize}
|
|
52
|
+
* />
|
|
53
|
+
*
|
|
54
|
+
* // Compact (no first/last, no page-size select)
|
|
55
|
+
* <Pagination
|
|
56
|
+
* page={page}
|
|
57
|
+
* pageSize={20}
|
|
58
|
+
* total={120}
|
|
59
|
+
* onPageChange={setPage}
|
|
60
|
+
* showFirstLast={false}
|
|
61
|
+
* showPageSize={false}
|
|
62
|
+
* />
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a stable list of pages + ellipsis placeholders that fits a
|
|
68
|
+
* pagination row of `boundaryCount + 2 + siblingCount * 2 + 1` cells.
|
|
69
|
+
* Returns `number` entries (1-indexed) and `'ellipsis-left'` /
|
|
70
|
+
* `'ellipsis-right'` placeholders that the renderer paints as `…`.
|
|
71
|
+
*
|
|
72
|
+
* Exported for tests + consumer reuse (e.g. a server-side rendered
|
|
73
|
+
* pagination indicator that needs the same shape).
|
|
74
|
+
*/
|
|
75
|
+
export function buildPaginationItems(
|
|
76
|
+
page: number,
|
|
77
|
+
totalPages: number,
|
|
78
|
+
siblingCount: number,
|
|
79
|
+
boundaryCount: number,
|
|
80
|
+
): Array<number | 'ellipsis-left' | 'ellipsis-right'> {
|
|
81
|
+
if (totalPages <= 0) return []
|
|
82
|
+
// When everything fits, just list every page.
|
|
83
|
+
const totalSlots = boundaryCount * 2 + siblingCount * 2 + 3
|
|
84
|
+
if (totalPages <= totalSlots) {
|
|
85
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
|
86
|
+
}
|
|
87
|
+
const startPages = Array.from({ length: boundaryCount }, (_, i) => i + 1)
|
|
88
|
+
const endPages = Array.from(
|
|
89
|
+
{ length: boundaryCount },
|
|
90
|
+
(_, i) => totalPages - boundaryCount + 1 + i,
|
|
91
|
+
)
|
|
92
|
+
const siblingStart = Math.max(
|
|
93
|
+
Math.min(page - siblingCount, totalPages - boundaryCount - siblingCount * 2 - 1),
|
|
94
|
+
boundaryCount + 2,
|
|
95
|
+
)
|
|
96
|
+
const siblingEnd = Math.min(
|
|
97
|
+
Math.max(page + siblingCount, boundaryCount + siblingCount * 2 + 2),
|
|
98
|
+
endPages.length > 0 ? endPages[0] - 2 : totalPages - 1,
|
|
99
|
+
)
|
|
100
|
+
const middle: Array<number> = []
|
|
101
|
+
for (let i = siblingStart; i <= siblingEnd; i += 1) middle.push(i)
|
|
102
|
+
|
|
103
|
+
// Bridge between the start boundary and the sibling window. The
|
|
104
|
+
// ellipsis is only worth showing when ≥2 page numbers fall between
|
|
105
|
+
// them; if exactly 1 page is missing, render that single number
|
|
106
|
+
// instead of "…" (cleaner UX — same width, no information loss).
|
|
107
|
+
const result: Array<number | 'ellipsis-left' | 'ellipsis-right'> = []
|
|
108
|
+
for (const p of startPages) result.push(p)
|
|
109
|
+
if (siblingStart > boundaryCount + 2) {
|
|
110
|
+
result.push('ellipsis-left')
|
|
111
|
+
} else if (siblingStart === boundaryCount + 2) {
|
|
112
|
+
result.push(boundaryCount + 1)
|
|
113
|
+
}
|
|
114
|
+
for (const p of middle) result.push(p)
|
|
115
|
+
const firstEnd = endPages[0] ?? totalPages
|
|
116
|
+
if (siblingEnd < firstEnd - 2) {
|
|
117
|
+
result.push('ellipsis-right')
|
|
118
|
+
} else if (siblingEnd === firstEnd - 2) {
|
|
119
|
+
result.push(firstEnd - 1)
|
|
120
|
+
}
|
|
121
|
+
for (const p of endPages) result.push(p)
|
|
122
|
+
return result
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cellVariants = cva(
|
|
126
|
+
'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-medium outline-none transition-colors tabular-nums ' +
|
|
127
|
+
'focus-visible:shadow-focus ' +
|
|
128
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
129
|
+
{
|
|
130
|
+
variants: {
|
|
131
|
+
selected: {
|
|
132
|
+
true: 'bg-muted text-foreground',
|
|
133
|
+
false: 'bg-background text-foreground hover:bg-muted/40',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
defaultVariants: { selected: false },
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const navButtonVariants = cva(
|
|
141
|
+
'inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors ' +
|
|
142
|
+
'hover:bg-muted/40 hover:text-foreground ' +
|
|
143
|
+
'focus-visible:shadow-focus ' +
|
|
144
|
+
'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground',
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
export type PaginationProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
148
|
+
/** Current 1-indexed page. */
|
|
149
|
+
page: number
|
|
150
|
+
/** Items per page. */
|
|
151
|
+
pageSize: number
|
|
152
|
+
/** Total item count. Used to derive `Math.ceil(total / pageSize)` pages. */
|
|
153
|
+
total: number
|
|
154
|
+
/** Called when the user changes page. */
|
|
155
|
+
onPageChange: (next: number) => void
|
|
156
|
+
/** Called when the user changes page size. Optional — when omitted,
|
|
157
|
+
* the "X / page" select is hidden. */
|
|
158
|
+
onPageSizeChange?: (next: number) => void
|
|
159
|
+
/** Page size options for the select. Default `[10, 25, 50, 100]`. */
|
|
160
|
+
pageSizeOptions?: readonly number[]
|
|
161
|
+
/** Show the left "Page X of Y" indicator. Default `true`. */
|
|
162
|
+
showInfo?: boolean
|
|
163
|
+
/** Show the right "X / page" select. Default `true` when
|
|
164
|
+
* `onPageSizeChange` is provided; ignored otherwise. */
|
|
165
|
+
showPageSize?: boolean
|
|
166
|
+
/** Show ⏮ / ⏭ first / last buttons. Default `true`. */
|
|
167
|
+
showFirstLast?: boolean
|
|
168
|
+
/** Show ◀ / ▶ prev / next buttons. Default `true`. */
|
|
169
|
+
showPrevNext?: boolean
|
|
170
|
+
/** Pages on either side of the current page in the page list. Default `1`. */
|
|
171
|
+
siblingCount?: number
|
|
172
|
+
/** Pages pinned at each end of the page list. Default `1`. */
|
|
173
|
+
boundaryCount?: number
|
|
174
|
+
/** Block all interactions. Default `false`. */
|
|
175
|
+
disabled?: boolean
|
|
176
|
+
/** ARIA label for the navigation landmark. Default `"Pagination"`. */
|
|
177
|
+
'aria-label'?: string
|
|
178
|
+
/** Format the "Page X of Y" label. */
|
|
179
|
+
formatPageInfo?: (page: number, totalPages: number) => string
|
|
180
|
+
/** Format the "X / page" label. */
|
|
181
|
+
formatPageSizeLabel?: (pageSize: number) => string
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const Pagination = React.forwardRef<HTMLDivElement, PaginationProps>(
|
|
185
|
+
(
|
|
186
|
+
{
|
|
187
|
+
className,
|
|
188
|
+
page,
|
|
189
|
+
pageSize,
|
|
190
|
+
total,
|
|
191
|
+
onPageChange,
|
|
192
|
+
onPageSizeChange,
|
|
193
|
+
pageSizeOptions = [10, 25, 50, 100],
|
|
194
|
+
showInfo = true,
|
|
195
|
+
showPageSize: showPageSizeProp,
|
|
196
|
+
showFirstLast = true,
|
|
197
|
+
showPrevNext = true,
|
|
198
|
+
siblingCount = 1,
|
|
199
|
+
boundaryCount = 1,
|
|
200
|
+
disabled = false,
|
|
201
|
+
formatPageInfo,
|
|
202
|
+
formatPageSizeLabel,
|
|
203
|
+
...props
|
|
204
|
+
},
|
|
205
|
+
ref,
|
|
206
|
+
) => {
|
|
207
|
+
const t = useT()
|
|
208
|
+
const resolvedFormatPageInfo =
|
|
209
|
+
formatPageInfo ??
|
|
210
|
+
((p: number, total: number) =>
|
|
211
|
+
t('ui.pagination.info.pageOf', 'Page {page} of {total}', { page: p, total }))
|
|
212
|
+
const resolvedFormatPageSizeLabel =
|
|
213
|
+
formatPageSizeLabel ??
|
|
214
|
+
((size: number) =>
|
|
215
|
+
t('ui.pagination.itemsPerPage.label', '{size} / page', { size }))
|
|
216
|
+
const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)))
|
|
217
|
+
const safePage = Math.min(Math.max(1, page), totalPages)
|
|
218
|
+
const items = React.useMemo(
|
|
219
|
+
() => buildPaginationItems(safePage, totalPages, siblingCount, boundaryCount),
|
|
220
|
+
[safePage, totalPages, siblingCount, boundaryCount],
|
|
221
|
+
)
|
|
222
|
+
const showPageSize = showPageSizeProp ?? Boolean(onPageSizeChange)
|
|
223
|
+
|
|
224
|
+
const goTo = React.useCallback(
|
|
225
|
+
(next: number) => {
|
|
226
|
+
if (disabled) return
|
|
227
|
+
const bounded = Math.min(Math.max(1, next), totalPages)
|
|
228
|
+
if (bounded !== safePage) onPageChange(bounded)
|
|
229
|
+
},
|
|
230
|
+
[disabled, onPageChange, safePage, totalPages],
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<nav
|
|
235
|
+
ref={ref}
|
|
236
|
+
data-slot="pagination"
|
|
237
|
+
aria-label={props['aria-label'] ?? t('ui.pagination.landmark.ariaLabel', 'Pagination')}
|
|
238
|
+
className={cn('flex w-full items-center justify-between gap-6', className)}
|
|
239
|
+
{...props}
|
|
240
|
+
>
|
|
241
|
+
{showInfo ? (
|
|
242
|
+
<div
|
|
243
|
+
data-slot="pagination-info"
|
|
244
|
+
className="shrink-0 text-sm text-muted-foreground tabular-nums"
|
|
245
|
+
>
|
|
246
|
+
{resolvedFormatPageInfo(safePage, totalPages)}
|
|
247
|
+
</div>
|
|
248
|
+
) : (
|
|
249
|
+
<div />
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
<div
|
|
253
|
+
data-slot="pagination-controls"
|
|
254
|
+
className="inline-flex items-center gap-2"
|
|
255
|
+
>
|
|
256
|
+
{showFirstLast ? (
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
data-slot="pagination-first"
|
|
260
|
+
aria-label={t('ui.pagination.first.ariaLabel', 'First page')}
|
|
261
|
+
disabled={disabled || safePage <= 1}
|
|
262
|
+
onClick={() => goTo(1)}
|
|
263
|
+
className={cn(navButtonVariants())}
|
|
264
|
+
>
|
|
265
|
+
<ChevronsLeft aria-hidden="true" className="size-5" />
|
|
266
|
+
</button>
|
|
267
|
+
) : null}
|
|
268
|
+
{showPrevNext ? (
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
data-slot="pagination-prev"
|
|
272
|
+
aria-label={t('ui.pagination.previous.ariaLabel', 'Previous page')}
|
|
273
|
+
disabled={disabled || safePage <= 1}
|
|
274
|
+
onClick={() => goTo(safePage - 1)}
|
|
275
|
+
className={cn(navButtonVariants())}
|
|
276
|
+
>
|
|
277
|
+
<ChevronLeft aria-hidden="true" className="size-5" />
|
|
278
|
+
</button>
|
|
279
|
+
) : null}
|
|
280
|
+
|
|
281
|
+
<ol
|
|
282
|
+
data-slot="pagination-pages"
|
|
283
|
+
className="inline-flex items-center gap-2 list-none"
|
|
284
|
+
>
|
|
285
|
+
{items.map((entry, index) => {
|
|
286
|
+
if (entry === 'ellipsis-left' || entry === 'ellipsis-right') {
|
|
287
|
+
return (
|
|
288
|
+
<li
|
|
289
|
+
key={`${entry}-${index}`}
|
|
290
|
+
data-slot="pagination-ellipsis"
|
|
291
|
+
aria-hidden="true"
|
|
292
|
+
className="inline-flex size-8 items-center justify-center text-sm text-muted-foreground"
|
|
293
|
+
>
|
|
294
|
+
…
|
|
295
|
+
</li>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
const selected = entry === safePage
|
|
299
|
+
return (
|
|
300
|
+
<li key={entry}>
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
data-slot="pagination-page"
|
|
304
|
+
data-state={selected ? 'on' : 'off'}
|
|
305
|
+
aria-current={selected ? 'page' : undefined}
|
|
306
|
+
aria-label={
|
|
307
|
+
selected
|
|
308
|
+
? t('ui.pagination.page.currentAriaLabel', 'Page {page}, current page', { page: entry })
|
|
309
|
+
: t('ui.pagination.page.goToAriaLabel', 'Go to page {page}', { page: entry })
|
|
310
|
+
}
|
|
311
|
+
disabled={disabled}
|
|
312
|
+
onClick={() => goTo(entry)}
|
|
313
|
+
className={cn(cellVariants({ selected }))}
|
|
314
|
+
>
|
|
315
|
+
{entry}
|
|
316
|
+
</button>
|
|
317
|
+
</li>
|
|
318
|
+
)
|
|
319
|
+
})}
|
|
320
|
+
</ol>
|
|
321
|
+
|
|
322
|
+
{showPrevNext ? (
|
|
323
|
+
<button
|
|
324
|
+
type="button"
|
|
325
|
+
data-slot="pagination-next"
|
|
326
|
+
aria-label={t('ui.pagination.next.ariaLabel', 'Next page')}
|
|
327
|
+
disabled={disabled || safePage >= totalPages}
|
|
328
|
+
onClick={() => goTo(safePage + 1)}
|
|
329
|
+
className={cn(navButtonVariants())}
|
|
330
|
+
>
|
|
331
|
+
<ChevronRight aria-hidden="true" className="size-5" />
|
|
332
|
+
</button>
|
|
333
|
+
) : null}
|
|
334
|
+
{showFirstLast ? (
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
data-slot="pagination-last"
|
|
338
|
+
aria-label={t('ui.pagination.last.ariaLabel', 'Last page')}
|
|
339
|
+
disabled={disabled || safePage >= totalPages}
|
|
340
|
+
onClick={() => goTo(totalPages)}
|
|
341
|
+
className={cn(navButtonVariants())}
|
|
342
|
+
>
|
|
343
|
+
<ChevronsRight aria-hidden="true" className="size-5" />
|
|
344
|
+
</button>
|
|
345
|
+
) : null}
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{showPageSize && onPageSizeChange ? (
|
|
349
|
+
<div data-slot="pagination-page-size" className="shrink-0">
|
|
350
|
+
<Select
|
|
351
|
+
value={String(pageSize)}
|
|
352
|
+
onValueChange={(next) => onPageSizeChange(Number(next))}
|
|
353
|
+
disabled={disabled}
|
|
354
|
+
>
|
|
355
|
+
<CompactSelectTrigger aria-label={t('ui.pagination.itemsPerPage.ariaLabel', 'Items per page')}>
|
|
356
|
+
<SelectValue />
|
|
357
|
+
</CompactSelectTrigger>
|
|
358
|
+
<SelectContent>
|
|
359
|
+
{pageSizeOptions.map((option) => (
|
|
360
|
+
<SelectItem key={option} value={String(option)}>
|
|
361
|
+
{resolvedFormatPageSizeLabel(option)}
|
|
362
|
+
</SelectItem>
|
|
363
|
+
))}
|
|
364
|
+
</SelectContent>
|
|
365
|
+
</Select>
|
|
366
|
+
</div>
|
|
367
|
+
) : (
|
|
368
|
+
<div />
|
|
369
|
+
)}
|
|
370
|
+
</nav>
|
|
371
|
+
)
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
Pagination.displayName = 'Pagination'
|
|
375
|
+
|
|
376
|
+
export { cellVariants as paginationCellVariants, navButtonVariants as paginationNavVariants }
|