@eggspot/ui 0.0.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/eslint.config.js +4 -0
- package/package.json +66 -0
- package/postcss.config.mjs +1 -0
- package/src/components/Button.machine.tsx +50 -0
- package/src/components/Button.tsx +249 -0
- package/src/components/Button.variants.tsx +186 -0
- package/src/components/ButtonGroup.tsx +56 -0
- package/src/components/Calendar.tsx +275 -0
- package/src/components/Calendar.utils.tsx +22 -0
- package/src/components/Checkbox.tsx +199 -0
- package/src/components/ConfirmDialog.tsx +183 -0
- package/src/components/DashboardLayout/DashboardLayout.tsx +348 -0
- package/src/components/DashboardLayout/SidebarNav.tsx +509 -0
- package/src/components/DashboardLayout/index.ts +33 -0
- package/src/components/DataTable/DataTable.tsx +557 -0
- package/src/components/DataTable/DataTableColumnHeader.tsx +122 -0
- package/src/components/DataTable/DataTableDisplaySettings.tsx +265 -0
- package/src/components/DataTable/DataTableFloatingBar.tsx +44 -0
- package/src/components/DataTable/DataTablePagination.tsx +168 -0
- package/src/components/DataTable/DataTableStates.tsx +69 -0
- package/src/components/DataTable/DataTableToolbarContainer.tsx +47 -0
- package/src/components/DataTable/hooks/use-data-table-settings.ts +101 -0
- package/src/components/DataTable/index.ts +7 -0
- package/src/components/DataTable/types/data-table.ts +97 -0
- package/src/components/DatePicker.tsx +213 -0
- package/src/components/DatePicker.utils.tsx +38 -0
- package/src/components/Datefield.tsx +109 -0
- package/src/components/Datefield.utils.ts +10 -0
- package/src/components/Dialog.tsx +167 -0
- package/src/components/Field.tsx +49 -0
- package/src/components/Filter/Filter.store.tsx +122 -0
- package/src/components/Filter/Filter.tsx +11 -0
- package/src/components/Filter/Filter.types.ts +107 -0
- package/src/components/Filter/FilterBar.tsx +38 -0
- package/src/components/Filter/FilterBuilder.tsx +158 -0
- package/src/components/Filter/FilterField/DateModeRowValue.tsx +250 -0
- package/src/components/Filter/FilterField/FilterAsyncSelect.tsx +191 -0
- package/src/components/Filter/FilterField/FilterDateMode.tsx +241 -0
- package/src/components/Filter/FilterField/FilterDateRange.tsx +169 -0
- package/src/components/Filter/FilterField/FilterSelect.tsx +208 -0
- package/src/components/Filter/FilterField/FilterSingleDate.tsx +277 -0
- package/src/components/Filter/FilterField/OptionItem.tsx +112 -0
- package/src/components/Filter/FilterField/index.ts +6 -0
- package/src/components/Filter/FilterRow.tsx +527 -0
- package/src/components/Filter/index.ts +17 -0
- package/src/components/Form.tsx +195 -0
- package/src/components/Heading.tsx +41 -0
- package/src/components/Input.tsx +221 -0
- package/src/components/InputOTP.tsx +78 -0
- package/src/components/Label.tsx +65 -0
- package/src/components/Layout.tsx +129 -0
- package/src/components/ListBox.tsx +97 -0
- package/src/components/Menu.tsx +152 -0
- package/src/components/NativeSelect.tsx +77 -0
- package/src/components/NumberInput.tsx +114 -0
- package/src/components/Popover.tsx +44 -0
- package/src/components/Provider.tsx +22 -0
- package/src/components/RadioGroup.tsx +191 -0
- package/src/components/Resizable.tsx +71 -0
- package/src/components/ScrollArea.tsx +57 -0
- package/src/components/Select.tsx +626 -0
- package/src/components/Select.utils.tsx +64 -0
- package/src/components/Separator.tsx +25 -0
- package/src/components/Sheet.tsx +147 -0
- package/src/components/Sonner.tsx +96 -0
- package/src/components/Spinner.tsx +30 -0
- package/src/components/Switch.tsx +51 -0
- package/src/components/Text.tsx +35 -0
- package/src/components/Tooltip.tsx +58 -0
- package/src/consts/config.ts +2 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/lib/utils.ts +10 -0
- package/tsconfig.json +11 -0
- package/tsconfig.lint.json +8 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
closestCenter,
|
|
4
|
+
DndContext,
|
|
5
|
+
DragEndEvent,
|
|
6
|
+
DragOverlay,
|
|
7
|
+
DragStartEvent,
|
|
8
|
+
KeyboardSensor,
|
|
9
|
+
PointerSensor,
|
|
10
|
+
TouchSensor,
|
|
11
|
+
useSensor,
|
|
12
|
+
useSensors,
|
|
13
|
+
} from "@dnd-kit/core"
|
|
14
|
+
import {
|
|
15
|
+
arrayMove,
|
|
16
|
+
SortableContext,
|
|
17
|
+
sortableKeyboardCoordinates,
|
|
18
|
+
useSortable,
|
|
19
|
+
verticalListSortingStrategy,
|
|
20
|
+
} from "@dnd-kit/sortable"
|
|
21
|
+
import { CSS } from "@dnd-kit/utilities"
|
|
22
|
+
import { Button } from "@eggspot/ui/components/Button"
|
|
23
|
+
import {
|
|
24
|
+
Popover,
|
|
25
|
+
PopoverDialog,
|
|
26
|
+
PopoverTrigger,
|
|
27
|
+
} from "@eggspot/ui/components/Popover"
|
|
28
|
+
import { ScrollArea } from "@eggspot/ui/components/ScrollArea"
|
|
29
|
+
import { Switch } from "@eggspot/ui/components/Switch"
|
|
30
|
+
import { cn } from "@eggspot/ui/lib/utils"
|
|
31
|
+
import { Table } from "@tanstack/react-table"
|
|
32
|
+
import { GripVertical, Pin, RotateCcw, Settings2 } from "lucide-react"
|
|
33
|
+
|
|
34
|
+
import { Heading } from "../Heading"
|
|
35
|
+
|
|
36
|
+
interface DataTableDisplaySettingsProps<TData> {
|
|
37
|
+
table: Table<TData>
|
|
38
|
+
onResetToDefaults: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SortableColumnItemProps {
|
|
42
|
+
id: string
|
|
43
|
+
label: string
|
|
44
|
+
isVisible: boolean
|
|
45
|
+
isPinned: "left" | "right" | false
|
|
46
|
+
onToggleVisibility?: () => void
|
|
47
|
+
onPin?: (position: "left" | "right" | false) => void
|
|
48
|
+
className?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toNormalText(str: string) {
|
|
52
|
+
return (
|
|
53
|
+
str
|
|
54
|
+
// convert camelCase → camel Case
|
|
55
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
56
|
+
// replace snake_case and kebab-case
|
|
57
|
+
.replace(/[-_]/g, " ")
|
|
58
|
+
// collapse repeated spaces
|
|
59
|
+
.replace(/\s+/g, " ")
|
|
60
|
+
.trim()
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function SortableColumnItem({
|
|
66
|
+
id,
|
|
67
|
+
label,
|
|
68
|
+
isVisible,
|
|
69
|
+
isPinned,
|
|
70
|
+
onToggleVisibility,
|
|
71
|
+
onPin,
|
|
72
|
+
className,
|
|
73
|
+
}: SortableColumnItemProps) {
|
|
74
|
+
const {
|
|
75
|
+
attributes,
|
|
76
|
+
listeners,
|
|
77
|
+
setNodeRef,
|
|
78
|
+
transform,
|
|
79
|
+
transition,
|
|
80
|
+
isDragging,
|
|
81
|
+
} = useSortable({ id })
|
|
82
|
+
|
|
83
|
+
const style = {
|
|
84
|
+
transform: CSS.Transform.toString(transform),
|
|
85
|
+
transition,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
ref={setNodeRef}
|
|
91
|
+
style={style}
|
|
92
|
+
className={cn(
|
|
93
|
+
"flex items-center gap-2 px-2 py-1.5",
|
|
94
|
+
className,
|
|
95
|
+
isDragging && "bg-accent-9/30 *:opacity-0"
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<button
|
|
99
|
+
{...attributes}
|
|
100
|
+
{...listeners}
|
|
101
|
+
className="hover:bg-gray-3 cursor-grab rounded p-1 active:cursor-grabbing"
|
|
102
|
+
>
|
|
103
|
+
<GripVertical className="text-gray-11 h-4 w-4" />
|
|
104
|
+
</button>
|
|
105
|
+
<span className="flex-1 truncate text-sm capitalize">
|
|
106
|
+
{toNormalText(label)}
|
|
107
|
+
</span>
|
|
108
|
+
<div className="flex items-center gap-1">
|
|
109
|
+
<Button
|
|
110
|
+
variant="ghost"
|
|
111
|
+
size="sm"
|
|
112
|
+
className={cn("h-6 w-6 p-0", isPinned === "left" && "text-accent-9")}
|
|
113
|
+
onClick={() => onPin?.(isPinned === "left" ? false : "left")}
|
|
114
|
+
tooltip="Pin to left"
|
|
115
|
+
tooltipDelay={1000}
|
|
116
|
+
>
|
|
117
|
+
<Pin className="size-3 rotate-45" />
|
|
118
|
+
</Button>
|
|
119
|
+
<Button
|
|
120
|
+
variant="ghost"
|
|
121
|
+
size="sm"
|
|
122
|
+
className={cn("h-6 w-6 p-0", isPinned === "right" && "text-accent-9")}
|
|
123
|
+
onClick={() => onPin?.(isPinned === "right" ? false : "right")}
|
|
124
|
+
tooltip="Pin to right"
|
|
125
|
+
tooltipDelay={1000}
|
|
126
|
+
>
|
|
127
|
+
<Pin className="size-3 -rotate-45" />
|
|
128
|
+
</Button>
|
|
129
|
+
<Switch
|
|
130
|
+
isSelected={isVisible}
|
|
131
|
+
onChange={onToggleVisibility}
|
|
132
|
+
className="scale-75"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function DataTableDisplaySettings<TData>({
|
|
140
|
+
table,
|
|
141
|
+
onResetToDefaults,
|
|
142
|
+
}: DataTableDisplaySettingsProps<TData>) {
|
|
143
|
+
const [activeId, setActiveId] = useState<string | null>(null)
|
|
144
|
+
|
|
145
|
+
const sensors = useSensors(
|
|
146
|
+
useSensor(PointerSensor),
|
|
147
|
+
useSensor(TouchSensor),
|
|
148
|
+
useSensor(KeyboardSensor, {
|
|
149
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const allColumns = table
|
|
154
|
+
.getAllColumns()
|
|
155
|
+
.filter((column) => column.id !== "select" && column.getCanHide())
|
|
156
|
+
const totalItemsHeight = allColumns.length * 40 + 8
|
|
157
|
+
|
|
158
|
+
const columnOrder = table.getState().columnOrder
|
|
159
|
+
const orderedColumns =
|
|
160
|
+
columnOrder.length > 0
|
|
161
|
+
? columnOrder
|
|
162
|
+
.map((id) => allColumns.find((col) => col.id === id))
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
: allColumns
|
|
165
|
+
|
|
166
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
167
|
+
const { active, over } = event
|
|
168
|
+
|
|
169
|
+
if (over && active.id !== over.id) {
|
|
170
|
+
setActiveId(null)
|
|
171
|
+
const oldIndex = columnOrder.indexOf(active.id as string)
|
|
172
|
+
const newIndex = columnOrder.indexOf(over.id as string)
|
|
173
|
+
const newOrder = arrayMove(columnOrder, oldIndex, newIndex)
|
|
174
|
+
table.setColumnOrder(newOrder)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handleSortStart = (event: DragStartEvent) => {
|
|
179
|
+
setActiveId(event.active.id as string)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<PopoverTrigger>
|
|
184
|
+
<Button variant="outline" size="sm" className="gap-2">
|
|
185
|
+
<Settings2 className="h-4 w-4" />
|
|
186
|
+
Display
|
|
187
|
+
</Button>
|
|
188
|
+
<Popover className="w-72 overflow-hidden" placement="bottom right">
|
|
189
|
+
<PopoverDialog className="p-0">
|
|
190
|
+
<div className="flex items-center justify-between border-b p-3">
|
|
191
|
+
<Heading variant="subtitle1">Display Settings</Heading>
|
|
192
|
+
<Button
|
|
193
|
+
variant="ghost"
|
|
194
|
+
size="sm"
|
|
195
|
+
className="h-8"
|
|
196
|
+
onClick={onResetToDefaults}
|
|
197
|
+
leftIcon={<RotateCcw />}
|
|
198
|
+
>
|
|
199
|
+
Reset
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
<ScrollArea
|
|
203
|
+
className="max-h-[300px]"
|
|
204
|
+
style={{ height: totalItemsHeight }}
|
|
205
|
+
>
|
|
206
|
+
<div className="py-1">
|
|
207
|
+
<DndContext
|
|
208
|
+
sensors={sensors}
|
|
209
|
+
collisionDetection={closestCenter}
|
|
210
|
+
onDragStart={handleSortStart}
|
|
211
|
+
onDragEnd={handleDragEnd}
|
|
212
|
+
>
|
|
213
|
+
<SortableContext
|
|
214
|
+
items={orderedColumns.map((col) => col!.id)}
|
|
215
|
+
strategy={verticalListSortingStrategy}
|
|
216
|
+
>
|
|
217
|
+
{orderedColumns.map((column) => {
|
|
218
|
+
if (!column) return null
|
|
219
|
+
return (
|
|
220
|
+
<SortableColumnItem
|
|
221
|
+
key={column.id}
|
|
222
|
+
id={column.id}
|
|
223
|
+
label={
|
|
224
|
+
typeof column.columnDef.header === "string"
|
|
225
|
+
? column.columnDef.header
|
|
226
|
+
: column.id
|
|
227
|
+
}
|
|
228
|
+
isVisible={column.getIsVisible()}
|
|
229
|
+
isPinned={column.getIsPinned()}
|
|
230
|
+
onToggleVisibility={() =>
|
|
231
|
+
column.toggleVisibility(!column.getIsVisible())
|
|
232
|
+
}
|
|
233
|
+
onPin={(position) => column.pin(position)}
|
|
234
|
+
/>
|
|
235
|
+
)
|
|
236
|
+
})}
|
|
237
|
+
</SortableContext>
|
|
238
|
+
<DragOverlay>
|
|
239
|
+
{activeId &&
|
|
240
|
+
orderedColumns.find((col) => col?.id === activeId) ? (
|
|
241
|
+
<SortableColumnItem
|
|
242
|
+
id={activeId}
|
|
243
|
+
label={activeId}
|
|
244
|
+
isVisible={
|
|
245
|
+
orderedColumns
|
|
246
|
+
.find((col) => col?.id === activeId)
|
|
247
|
+
?.getIsVisible() ?? false
|
|
248
|
+
}
|
|
249
|
+
isPinned={
|
|
250
|
+
orderedColumns
|
|
251
|
+
.find((col) => col?.id === activeId)
|
|
252
|
+
?.getIsPinned() ?? false
|
|
253
|
+
}
|
|
254
|
+
className="bg-gray-3 rounded shadow-sm"
|
|
255
|
+
/>
|
|
256
|
+
) : null}
|
|
257
|
+
</DragOverlay>
|
|
258
|
+
</DndContext>
|
|
259
|
+
</div>
|
|
260
|
+
</ScrollArea>
|
|
261
|
+
</PopoverDialog>
|
|
262
|
+
</Popover>
|
|
263
|
+
</PopoverTrigger>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Button } from "@eggspot/ui/components/Button"
|
|
2
|
+
import { Separator } from "@eggspot/ui/components/Separator"
|
|
3
|
+
import { cn } from "@eggspot/ui/lib/utils"
|
|
4
|
+
import { X } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
interface DataTableFloatingBarProps {
|
|
7
|
+
selectedCount: number
|
|
8
|
+
onClearSelection: () => void
|
|
9
|
+
children?: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DataTableFloatingBar({
|
|
13
|
+
selectedCount,
|
|
14
|
+
onClearSelection,
|
|
15
|
+
children,
|
|
16
|
+
}: DataTableFloatingBarProps) {
|
|
17
|
+
if (selectedCount === 0) return null
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={cn(
|
|
22
|
+
"fixed bottom-18 left-1/2 z-10 -translate-x-1/2",
|
|
23
|
+
"bg-gray-2 text-gray-12",
|
|
24
|
+
"rounded-xl border p-2 shadow-2xl dark:shadow-[0_25px_50px_0px_rgba(0,0,0,0.2),0_15px_25px_-2px_rgba(0,0,0,0.1)]",
|
|
25
|
+
"flex items-center gap-4",
|
|
26
|
+
"animate-in fade-in slide-in-from-bottom-4"
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
<div className="flex items-center gap-1.5 pl-2">
|
|
30
|
+
<span className="text-sm font-medium">{selectedCount} selected</span>
|
|
31
|
+
<Button
|
|
32
|
+
variant="ghost"
|
|
33
|
+
size="sm"
|
|
34
|
+
className="text-floating-bar-foreground/70 hover:text-floating-bar-foreground hover:bg-floating-bar-foreground/10 h-7 w-7 p-0"
|
|
35
|
+
onClick={onClearSelection}
|
|
36
|
+
>
|
|
37
|
+
<X className="h-4 w-4" />
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
<Separator orientation="vertical" className="bg-gray-6 h-6" />
|
|
41
|
+
<div className="flex items-center gap-2">{children}</div>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { RefObject } from "react"
|
|
2
|
+
import { Button } from "@eggspot/ui/components/Button"
|
|
3
|
+
import { NumberInput } from "@eggspot/ui/components/NumberInput"
|
|
4
|
+
import { Select, SelectOption } from "@eggspot/ui/components/Select"
|
|
5
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
6
|
+
import { useMeasure } from "react-use"
|
|
7
|
+
|
|
8
|
+
interface DataTablePaginationProps {
|
|
9
|
+
pageIndex: number
|
|
10
|
+
pageSize: number
|
|
11
|
+
totalRows: number
|
|
12
|
+
onPageChange: (page: number) => void
|
|
13
|
+
onPageSizeChange: (size: number) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pageSizeOptions: SelectOption<number>[] = [
|
|
17
|
+
{ value: 10, label: "10" },
|
|
18
|
+
{ value: 20, label: "20" },
|
|
19
|
+
{ value: 50, label: "50" },
|
|
20
|
+
{ value: 100, label: "100" },
|
|
21
|
+
{ value: 200, label: "200" },
|
|
22
|
+
{ value: 1000, label: "1000" },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
export function DataTablePagination({
|
|
26
|
+
pageIndex,
|
|
27
|
+
pageSize,
|
|
28
|
+
totalRows,
|
|
29
|
+
onPageChange,
|
|
30
|
+
onPageSizeChange,
|
|
31
|
+
}: DataTablePaginationProps) {
|
|
32
|
+
const [containerRef, { width }] = useMeasure()
|
|
33
|
+
const pageCount = Math.ceil(totalRows / pageSize)
|
|
34
|
+
const canPreviousPage = pageIndex > 0
|
|
35
|
+
const canNextPage = pageIndex < pageCount - 1
|
|
36
|
+
|
|
37
|
+
const startRow = totalRows && pageIndex * pageSize + 1
|
|
38
|
+
const endRow = Math.min((pageIndex + 1) * pageSize, totalRows)
|
|
39
|
+
|
|
40
|
+
// Generate page numbers with ellipsis
|
|
41
|
+
const getPageNumbers = () => {
|
|
42
|
+
const pages: (number | "ellipsis")[] = []
|
|
43
|
+
const maxVisiblePages = 7
|
|
44
|
+
|
|
45
|
+
if (pageCount <= maxVisiblePages) {
|
|
46
|
+
for (let i = 0; i < pageCount; i++) {
|
|
47
|
+
pages.push(i)
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
pages.push(0)
|
|
51
|
+
|
|
52
|
+
if (pageIndex <= 2) {
|
|
53
|
+
// Near start: 1, 2, 3, 4, ..., last
|
|
54
|
+
for (let i = 1; i < 4; i++) {
|
|
55
|
+
pages.push(i)
|
|
56
|
+
}
|
|
57
|
+
pages.push("ellipsis")
|
|
58
|
+
pages.push(pageCount - 1)
|
|
59
|
+
} else if (pageIndex >= pageCount - 3) {
|
|
60
|
+
// Near end: 1, ..., last-3, last-2, last-1, last
|
|
61
|
+
pages.push("ellipsis")
|
|
62
|
+
for (let i = pageCount - 4; i < pageCount; i++) {
|
|
63
|
+
pages.push(i)
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Middle: 1, ..., prev, current, next, ..., last
|
|
67
|
+
pages.push("ellipsis")
|
|
68
|
+
pages.push(pageIndex - 1)
|
|
69
|
+
pages.push(pageIndex)
|
|
70
|
+
pages.push(pageIndex + 1)
|
|
71
|
+
pages.push("ellipsis")
|
|
72
|
+
pages.push(pageCount - 1)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return pages
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
ref={containerRef as unknown as RefObject<HTMLDivElement>}
|
|
82
|
+
className="border-border bg-card flex items-center justify-between border-t px-6 py-2.5"
|
|
83
|
+
>
|
|
84
|
+
{/* Per Page Select & Total Rows */}
|
|
85
|
+
<div className={"flex items-center gap-6 text-sm"}>
|
|
86
|
+
<div className="flex items-center gap-2">
|
|
87
|
+
<span className="text-gray-11 whitespace-nowrap">Rows per page</span>
|
|
88
|
+
<Select
|
|
89
|
+
triggerClassName="w-[80px]"
|
|
90
|
+
popoverProps={{ className: "w-[97px]" }}
|
|
91
|
+
isSearchable={false}
|
|
92
|
+
isClearable={false}
|
|
93
|
+
value={pageSizeOptions.find((option) => option.value === pageSize)}
|
|
94
|
+
onChange={(value) => {
|
|
95
|
+
onPageSizeChange(value.value)
|
|
96
|
+
}}
|
|
97
|
+
options={pageSizeOptions}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
{width > 1000 && (
|
|
101
|
+
<span className="text-gray-11 whitespace-nowrap">
|
|
102
|
+
Showing {startRow} to {endRow} of {totalRows}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Pagination */}
|
|
108
|
+
<div className="flex items-center gap-6">
|
|
109
|
+
{width > 1000 && (
|
|
110
|
+
<div className="flex items-center gap-1.5">
|
|
111
|
+
<Button
|
|
112
|
+
mode="icon"
|
|
113
|
+
variant="outline"
|
|
114
|
+
onClick={() => onPageChange(pageIndex - 1)}
|
|
115
|
+
isDisabled={!canPreviousPage}
|
|
116
|
+
>
|
|
117
|
+
<ChevronLeft className="h-4 w-4" />
|
|
118
|
+
</Button>
|
|
119
|
+
|
|
120
|
+
{getPageNumbers().map((page, index) =>
|
|
121
|
+
page === "ellipsis" ? (
|
|
122
|
+
<span key={`ellipsis-${index}`} className="text-gray-11 px-2">
|
|
123
|
+
...
|
|
124
|
+
</span>
|
|
125
|
+
) : (
|
|
126
|
+
<Button
|
|
127
|
+
key={page}
|
|
128
|
+
mode="icon"
|
|
129
|
+
className="transition-none"
|
|
130
|
+
onClick={() => onPageChange(page)}
|
|
131
|
+
variant={page === pageIndex ? "solid" : "outline"}
|
|
132
|
+
>
|
|
133
|
+
{page + 1}
|
|
134
|
+
</Button>
|
|
135
|
+
)
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
<Button
|
|
139
|
+
mode="icon"
|
|
140
|
+
variant="outline"
|
|
141
|
+
isDisabled={!canNextPage}
|
|
142
|
+
onClick={() => onPageChange(pageIndex + 1)}
|
|
143
|
+
>
|
|
144
|
+
<ChevronRight className="h-4 w-4" />
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
<div className="flex items-center gap-2">
|
|
150
|
+
<span className="text-gray-11 text-sm">Go to</span>
|
|
151
|
+
<NumberInput
|
|
152
|
+
showStepper
|
|
153
|
+
minValue={1}
|
|
154
|
+
maxValue={pageCount}
|
|
155
|
+
value={pageIndex + 1}
|
|
156
|
+
onChange={(value) => onPageChange(value - 1)}
|
|
157
|
+
className="h-8 w-16 text-center"
|
|
158
|
+
aria-label="Go to page"
|
|
159
|
+
onFocus={(e) => {
|
|
160
|
+
const input = e.target as HTMLInputElement
|
|
161
|
+
input.select()
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Button } from "@eggspot/ui/components/Button"
|
|
2
|
+
import { AlertCircle, Inbox } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { Spinner } from "../Spinner"
|
|
5
|
+
|
|
6
|
+
function StatesContainer({ children }: { children: React.ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<tbody>
|
|
9
|
+
<tr className="h-20">
|
|
10
|
+
<td className="absolute inset-0 top-10 flex flex-col items-center pt-20">
|
|
11
|
+
{children}
|
|
12
|
+
</td>
|
|
13
|
+
</tr>
|
|
14
|
+
</tbody>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DataTableLoading() {
|
|
19
|
+
return (
|
|
20
|
+
<StatesContainer>
|
|
21
|
+
<Spinner className="mb-4 size-7" />
|
|
22
|
+
</StatesContainer>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DataTableEmptyProps {
|
|
27
|
+
title?: string
|
|
28
|
+
description?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DataTableEmpty({
|
|
32
|
+
title = "No results found",
|
|
33
|
+
description = "Try adjusting your search or filters to find what you're looking for.",
|
|
34
|
+
}: DataTableEmptyProps) {
|
|
35
|
+
return (
|
|
36
|
+
<StatesContainer>
|
|
37
|
+
<div className="bg-gray-3 mb-3 rounded-lg p-3">
|
|
38
|
+
<Inbox className="size-7 stroke-[1.5px]" />
|
|
39
|
+
</div>
|
|
40
|
+
<h3 className="mb-1 text-lg font-semibold">{title}</h3>
|
|
41
|
+
<p className="text-gray-11 max-w-sm text-center">{description}</p>
|
|
42
|
+
</StatesContainer>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface DataTableErrorProps {
|
|
47
|
+
message?: string
|
|
48
|
+
onRetry?: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function DataTableError({
|
|
52
|
+
message = "Something went wrong while loading the data.",
|
|
53
|
+
onRetry,
|
|
54
|
+
}: DataTableErrorProps) {
|
|
55
|
+
return (
|
|
56
|
+
<StatesContainer>
|
|
57
|
+
<div className="bg-error-9/10 mb-3 rounded-lg p-3">
|
|
58
|
+
<AlertCircle className="text-error-11 size-7 stroke-[1.5px]" />
|
|
59
|
+
</div>
|
|
60
|
+
<h3 className="mb-1 text-lg font-semibold">Error loading data</h3>
|
|
61
|
+
<p className="text-gray-11 mb-4 max-w-sm text-center">{message}</p>
|
|
62
|
+
{onRetry && (
|
|
63
|
+
<Button variant="outline" onClick={onRetry}>
|
|
64
|
+
Try again
|
|
65
|
+
</Button>
|
|
66
|
+
)}
|
|
67
|
+
</StatesContainer>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { Table } from "@tanstack/react-table"
|
|
3
|
+
|
|
4
|
+
import { DebouncedSearchInput } from "../Input"
|
|
5
|
+
import { Separator } from "../Separator"
|
|
6
|
+
|
|
7
|
+
interface DataTableToolbarContainerProps<TData> {
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
table: Table<TData>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DataTableToolbarContainer<TData>({
|
|
13
|
+
children,
|
|
14
|
+
table,
|
|
15
|
+
}: DataTableToolbarContainerProps<TData>) {
|
|
16
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
17
|
+
|
|
18
|
+
// TODO: implement the logic to open and close the toolbar
|
|
19
|
+
return (
|
|
20
|
+
<div className="border-b" data-state={isOpen ? "open" : "closed"}>
|
|
21
|
+
<div className="flex items-center px-5">
|
|
22
|
+
{/* <Button
|
|
23
|
+
size="sm"
|
|
24
|
+
mode="icon"
|
|
25
|
+
variant="ghost"
|
|
26
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
27
|
+
>
|
|
28
|
+
{isOpen ? <Minimize2Icon /> : <Maximize2Icon />}
|
|
29
|
+
</Button> */}
|
|
30
|
+
<div className="flex-1">
|
|
31
|
+
<DebouncedSearchInput
|
|
32
|
+
className="h-10 rounded-none bg-transparent ring-0!"
|
|
33
|
+
value={table.getState().globalFilter}
|
|
34
|
+
onChange={(v) => {
|
|
35
|
+
table.setGlobalFilter(v)
|
|
36
|
+
}}
|
|
37
|
+
placeholder="Search users by name, email, role..."
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<Separator />
|
|
42
|
+
<div className="flex items-center justify-between px-6 py-1.5">
|
|
43
|
+
{children}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
ColumnPinningState,
|
|
4
|
+
ColumnSizingState,
|
|
5
|
+
VisibilityState,
|
|
6
|
+
} from "@tanstack/react-table"
|
|
7
|
+
|
|
8
|
+
import { DisplaySettings } from "../types/data-table"
|
|
9
|
+
|
|
10
|
+
const STORAGE_PREFIX = "data-table-settings-"
|
|
11
|
+
|
|
12
|
+
interface UseDataTableSettingsProps {
|
|
13
|
+
storageKey: string
|
|
14
|
+
defaultColumnVisibility?: VisibilityState
|
|
15
|
+
defaultColumnSizing?: ColumnSizingState
|
|
16
|
+
defaultColumnPinning?: ColumnPinningState
|
|
17
|
+
defaultColumnOrder?: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useDataTableSettings({
|
|
21
|
+
storageKey,
|
|
22
|
+
defaultColumnVisibility = {},
|
|
23
|
+
defaultColumnSizing = {},
|
|
24
|
+
defaultColumnPinning = { left: [], right: [] },
|
|
25
|
+
defaultColumnOrder = [],
|
|
26
|
+
}: UseDataTableSettingsProps) {
|
|
27
|
+
const fullStorageKey = STORAGE_PREFIX + storageKey
|
|
28
|
+
|
|
29
|
+
const getStoredSettings = useCallback((): DisplaySettings | null => {
|
|
30
|
+
try {
|
|
31
|
+
const stored = localStorage.getItem(fullStorageKey)
|
|
32
|
+
if (stored) {
|
|
33
|
+
return JSON.parse(stored)
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("Failed to parse stored settings:", error)
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}, [fullStorageKey])
|
|
40
|
+
|
|
41
|
+
const [settings, setSettings] = useState<DisplaySettings>(() => {
|
|
42
|
+
const stored = getStoredSettings()
|
|
43
|
+
return (
|
|
44
|
+
stored || {
|
|
45
|
+
columnVisibility: defaultColumnVisibility,
|
|
46
|
+
columnSizing: defaultColumnSizing,
|
|
47
|
+
columnPinning: defaultColumnPinning,
|
|
48
|
+
columnOrder: defaultColumnOrder,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Persist settings to localStorage
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
try {
|
|
56
|
+
localStorage.setItem(fullStorageKey, JSON.stringify(settings))
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("Failed to save settings:", error)
|
|
59
|
+
}
|
|
60
|
+
}, [settings, fullStorageKey])
|
|
61
|
+
|
|
62
|
+
const updateColumnVisibility = useCallback((visibility: VisibilityState) => {
|
|
63
|
+
setSettings((prev) => ({ ...prev, columnVisibility: visibility }))
|
|
64
|
+
}, [])
|
|
65
|
+
|
|
66
|
+
const updateColumnSizing = useCallback((sizing: ColumnSizingState) => {
|
|
67
|
+
setSettings((prev) => ({ ...prev, columnSizing: sizing }))
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
const updateColumnPinning = useCallback((pinning: ColumnPinningState) => {
|
|
71
|
+
setSettings((prev) => ({ ...prev, columnPinning: pinning }))
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
const updateColumnOrder = useCallback((order: string[]) => {
|
|
75
|
+
setSettings((prev) => ({ ...prev, columnOrder: order }))
|
|
76
|
+
}, [])
|
|
77
|
+
|
|
78
|
+
const resetToDefaults = useCallback(() => {
|
|
79
|
+
const defaults: DisplaySettings = {
|
|
80
|
+
columnVisibility: defaultColumnVisibility,
|
|
81
|
+
columnSizing: defaultColumnSizing,
|
|
82
|
+
columnPinning: defaultColumnPinning,
|
|
83
|
+
columnOrder: defaultColumnOrder,
|
|
84
|
+
}
|
|
85
|
+
setSettings(defaults)
|
|
86
|
+
}, [
|
|
87
|
+
defaultColumnVisibility,
|
|
88
|
+
defaultColumnSizing,
|
|
89
|
+
defaultColumnPinning,
|
|
90
|
+
defaultColumnOrder,
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
settings,
|
|
95
|
+
updateColumnVisibility,
|
|
96
|
+
updateColumnSizing,
|
|
97
|
+
updateColumnPinning,
|
|
98
|
+
updateColumnOrder,
|
|
99
|
+
resetToDefaults,
|
|
100
|
+
}
|
|
101
|
+
}
|