@exxatdesignux/ui 0.0.6 → 0.0.7
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/bin/init.mjs +29 -0
- package/package.json +7 -2
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +485 -0
- package/template/Logo/Exxat_Prism.svg +39 -0
- package/template/Logo/Exxat_one.svg +36 -0
- package/template/README.md +58 -0
- package/template/app/(app)/compliance/page.tsx +10 -0
- package/template/app/(app)/dashboard/loading.tsx +18 -0
- package/template/app/(app)/dashboard/page.tsx +36 -0
- package/template/app/(app)/data-list/[id]/page.tsx +28 -0
- package/template/app/(app)/data-list/new/page.tsx +31 -0
- package/template/app/(app)/data-list/page.tsx +10 -0
- package/template/app/(app)/error.tsx +43 -0
- package/template/app/(app)/help/page.tsx +34 -0
- package/template/app/(app)/layout.tsx +54 -0
- package/template/app/(app)/loading.tsx +18 -0
- package/template/app/(app)/question-bank/page.tsx +10 -0
- package/template/app/(app)/rotations/page.tsx +15 -0
- package/template/app/(app)/settings/page.tsx +17 -0
- package/template/app/(app)/sites/all/page.tsx +13 -0
- package/template/app/(app)/team/page.tsx +10 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +1811 -0
- package/template/app/layout.tsx +95 -0
- package/template/app/page.tsx +9 -0
- package/template/components/.gitkeep +0 -0
- package/template/components/app-sidebar-dynamic.tsx +15 -0
- package/template/components/app-sidebar.tsx +901 -0
- package/template/components/ask-leo-composer.tsx +216 -0
- package/template/components/ask-leo-sidebar.tsx +509 -0
- package/template/components/chart-area-interactive.tsx +293 -0
- package/template/components/charts-overview.tsx +2321 -0
- package/template/components/command-menu-01.tsx +133 -0
- package/template/components/command-menu-02.tsx +386 -0
- package/template/components/command-menu.tsx +182 -0
- package/template/components/compliance-board-view.tsx +134 -0
- package/template/components/compliance-client.tsx +92 -0
- package/template/components/compliance-list-view.tsx +59 -0
- package/template/components/compliance-page-header.tsx +89 -0
- package/template/components/compliance-table.tsx +525 -0
- package/template/components/dashboard-onboarding-gallery.tsx +13 -0
- package/template/components/dashboard-onboarding.tsx +21 -0
- package/template/components/dashboard-promo-banner.tsx +67 -0
- package/template/components/dashboard-quota-progress-card.tsx +369 -0
- package/template/components/dashboard-report-charts.tsx +69 -0
- package/template/components/dashboard-section-heading.tsx +68 -0
- package/template/components/dashboard-tabs.tsx +598 -0
- package/template/components/data-list-client.tsx +239 -0
- package/template/components/data-list-table-cells.test.tsx +22 -0
- package/template/components/data-list-table-cells.tsx +173 -0
- package/template/components/data-list-table.tsx +879 -0
- package/template/components/data-table/filter-date-calendar.tsx +38 -0
- package/template/components/data-table/filter-text-value-input.tsx +77 -0
- package/template/components/data-table/index.tsx +1612 -0
- package/template/components/data-table/pagination.tsx +256 -0
- package/template/components/data-table/types.ts +91 -0
- package/template/components/data-table/use-table-state.ts +566 -0
- package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
- package/template/components/data-view-dashboard-charts-team.tsx +968 -0
- package/template/components/data-view-dashboard-charts.tsx +1668 -0
- package/template/components/data-views/board-card-primitives.tsx +93 -0
- package/template/components/data-views/index.ts +41 -0
- package/template/components/data-views/list-page-board-card.tsx +192 -0
- package/template/components/data-views/list-page-board-template.tsx +122 -0
- package/template/components/data-views/placement-board-card.tsx +262 -0
- package/template/components/export-drawer.tsx +375 -0
- package/template/components/exxat-product-logo.tsx +453 -0
- package/template/components/form-layout-01.tsx +131 -0
- package/template/components/getting-started.tsx +625 -0
- package/template/components/key-metrics.tsx +920 -0
- package/template/components/leo-insight-indicator.tsx +364 -0
- package/template/components/leo-typing-dots.tsx +121 -0
- package/template/components/list-hub-status-badge.tsx +51 -0
- package/template/components/list-page-dashboard-charts.tsx +18 -0
- package/template/components/nav-documents.tsx +89 -0
- package/template/components/nav-main.tsx +58 -0
- package/template/components/nav-secondary.tsx +64 -0
- package/template/components/nav-user.tsx +190 -0
- package/template/components/new-placement-back-btn.tsx +28 -0
- package/template/components/new-placement-form.tsx +1066 -0
- package/template/components/onboarding/index.ts +4 -0
- package/template/components/onboarding/onboarding-01.tsx +7 -0
- package/template/components/onboarding/onboarding-02.tsx +7 -0
- package/template/components/onboarding/onboarding-03.tsx +7 -0
- package/template/components/onboarding/onboarding-04.tsx +7 -0
- package/template/components/page-header.tsx +57 -0
- package/template/components/placement-detail.tsx +438 -0
- package/template/components/placements-board-view.tsx +404 -0
- package/template/components/placements-list-view.tsx +285 -0
- package/template/components/placements-page-header.tsx +160 -0
- package/template/components/placements-table-columns.tsx +639 -0
- package/template/components/product-switcher.tsx +116 -0
- package/template/components/question-bank-board-view.tsx +205 -0
- package/template/components/question-bank-client.tsx +77 -0
- package/template/components/question-bank-list-view.tsx +59 -0
- package/template/components/question-bank-page-header.tsx +89 -0
- package/template/components/question-bank-table.tsx +586 -0
- package/template/components/rotations-empty-state.tsx +47 -0
- package/template/components/rotations-panel-activator.tsx +8 -0
- package/template/components/secondary-nav.tsx +394 -0
- package/template/components/secondary-panel.tsx +239 -0
- package/template/components/section-cards.tsx +106 -0
- package/template/components/settings-appearance-card.tsx +424 -0
- package/template/components/settings-client.tsx +537 -0
- package/template/components/settings-form-row.tsx +42 -0
- package/template/components/sidebar-auto-collapse.tsx +23 -0
- package/template/components/sidebar-auto-open.tsx +18 -0
- package/template/components/sidebar-shell.tsx +37 -0
- package/template/components/site-header.tsx +93 -0
- package/template/components/sites-all-client.tsx +154 -0
- package/template/components/sites-board-view.tsx +67 -0
- package/template/components/sites-list-view.tsx +47 -0
- package/template/components/sites-table.tsx +312 -0
- package/template/components/system-banner-slot.tsx +66 -0
- package/template/components/table-properties/column-row.tsx +90 -0
- package/template/components/table-properties/draggable-list.ts +49 -0
- package/template/components/table-properties/drawer-button.tsx +231 -0
- package/template/components/table-properties/drawer.tsx +1102 -0
- package/template/components/table-properties/filter-card.tsx +251 -0
- package/template/components/table-properties/index.ts +22 -0
- package/template/components/table-properties/sort-card.tsx +59 -0
- package/template/components/table-properties/types.ts +124 -0
- package/template/components/task-list-panel.tsx +98 -0
- package/template/components/task-priority-badge.tsx +28 -0
- package/template/components/team-board-view.tsx +114 -0
- package/template/components/team-client.tsx +93 -0
- package/template/components/team-list-view.tsx +62 -0
- package/template/components/team-page-header.tsx +92 -0
- package/template/components/team-table.tsx +525 -0
- package/template/components/templates/list-page.tsx +576 -0
- package/template/components/templates/primary-page-template.tsx +56 -0
- package/template/components/theme-color-sync.tsx +32 -0
- package/template/components/theme-provider.tsx +71 -0
- package/template/components/tinted-icon-disc.tsx +53 -0
- package/template/components/ui/ai-thinking-surface.tsx +121 -0
- package/template/components/ui/avatar.tsx +1 -0
- package/template/components/ui/badge.tsx +1 -0
- package/template/components/ui/banner.tsx +1 -0
- package/template/components/ui/breadcrumb.tsx +1 -0
- package/template/components/ui/button.tsx +1 -0
- package/template/components/ui/calendar.tsx +1 -0
- package/template/components/ui/card.tsx +1 -0
- package/template/components/ui/chart.tsx +1 -0
- package/template/components/ui/checkbox.tsx +1 -0
- package/template/components/ui/coach-mark.tsx +1 -0
- package/template/components/ui/collapsible.tsx +1 -0
- package/template/components/ui/command.tsx +1 -0
- package/template/components/ui/date-picker-field.tsx +1 -0
- package/template/components/ui/dialog.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +159 -0
- package/template/components/ui/drag-handle-grip.tsx +1 -0
- package/template/components/ui/drawer.tsx +1 -0
- package/template/components/ui/dropdown-menu.tsx +1 -0
- package/template/components/ui/field.tsx +1 -0
- package/template/components/ui/form.tsx +1 -0
- package/template/components/ui/input-group.tsx +1 -0
- package/template/components/ui/input-mask.tsx +1 -0
- package/template/components/ui/input.tsx +1 -0
- package/template/components/ui/kbd.tsx +1 -0
- package/template/components/ui/label.tsx +1 -0
- package/template/components/ui/leo-icon.tsx +726 -0
- package/template/components/ui/payment-card-fields.tsx +1 -0
- package/template/components/ui/popover.tsx +1 -0
- package/template/components/ui/radio-group.tsx +1 -0
- package/template/components/ui/select.tsx +1 -0
- package/template/components/ui/selection-tile-grid.tsx +1 -0
- package/template/components/ui/separator.tsx +1 -0
- package/template/components/ui/sheet.tsx +1 -0
- package/template/components/ui/sidebar.tsx +1 -0
- package/template/components/ui/skeleton.tsx +1 -0
- package/template/components/ui/sonner.tsx +1 -0
- package/template/components/ui/status-badge.tsx +1 -0
- package/template/components/ui/table.tsx +1 -0
- package/template/components/ui/tabs.tsx +1 -0
- package/template/components/ui/textarea.tsx +1 -0
- package/template/components/ui/tip.tsx +1 -0
- package/template/components/ui/toggle-group.tsx +1 -0
- package/template/components/ui/toggle-switch.tsx +1 -0
- package/template/components/ui/toggle.tsx +1 -0
- package/template/components/ui/tooltip.tsx +1 -0
- package/template/components/ui/view-segmented-control.tsx +1 -0
- package/template/components.json +27 -0
- package/template/contexts/chart-variant-context.tsx +35 -0
- package/template/contexts/command-menu-context.tsx +28 -0
- package/template/contexts/dashboard-view-context.tsx +35 -0
- package/template/contexts/product-context.tsx +38 -0
- package/template/contexts/system-banner-context.tsx +127 -0
- package/template/docs/command-menu-pattern.md +45 -0
- package/template/docs/data-views-pattern.md +160 -0
- package/template/ecosystem.config.cjs +20 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fontawesome-subset.manifest.json +190 -0
- package/template/hooks/.gitkeep +0 -0
- package/template/hooks/use-app-theme.ts +1 -0
- package/template/hooks/use-coach-mark.ts +1 -0
- package/template/hooks/use-mobile.ts +1 -0
- package/template/hooks/use-mod-key-label.ts +1 -0
- package/template/lib/.gitkeep +0 -0
- package/template/lib/ask-leo-route-context.ts +133 -0
- package/template/lib/chart-keyboard-selection.test.ts +20 -0
- package/template/lib/chart-keyboard-selection.ts +17 -0
- package/template/lib/chart-line-dash.ts +16 -0
- package/template/lib/coach-mark-registry.ts +68 -0
- package/template/lib/command-menu-config.ts +127 -0
- package/template/lib/command-menu-search-data.ts +44 -0
- package/template/lib/conditional-rule-match.ts +32 -0
- package/template/lib/dashboard-customize-coach-mark.ts +18 -0
- package/template/lib/dashboard-layout-merge.ts +63 -0
- package/template/lib/data-list-display-options.ts +35 -0
- package/template/lib/data-list-persistence.ts +280 -0
- package/template/lib/data-list-view-surface.ts +58 -0
- package/template/lib/data-list-view.ts +29 -0
- package/template/lib/data-view-dashboard-storage.ts +101 -0
- package/template/lib/date-filter.ts +8 -0
- package/template/lib/dev-log.test.ts +28 -0
- package/template/lib/dev-log.ts +8 -0
- package/template/lib/editable-target.ts +10 -0
- package/template/lib/floating-sheet-panel.ts +72 -0
- package/template/lib/initials-from-name.ts +7 -0
- package/template/lib/list-page-table-properties.ts +52 -0
- package/template/lib/list-status-badges.ts +168 -0
- package/template/lib/logo-dev.ts +12 -0
- package/template/lib/mock/compliance-kpi.ts +61 -0
- package/template/lib/mock/compliance.ts +146 -0
- package/template/lib/mock/dashboard.ts +105 -0
- package/template/lib/mock/navigation.tsx +231 -0
- package/template/lib/mock/placements-kpi.ts +134 -0
- package/template/lib/mock/placements.ts +183 -0
- package/template/lib/mock/question-bank-kpi.ts +61 -0
- package/template/lib/mock/question-bank.ts +142 -0
- package/template/lib/mock/sites-directory.ts +16 -0
- package/template/lib/mock/sites-kpi.ts +25 -0
- package/template/lib/mock/team-kpi.ts +60 -0
- package/template/lib/mock/team.ts +118 -0
- package/template/lib/motion-ui.ts +17 -0
- package/template/lib/placement-board-card-layout.ts +79 -0
- package/template/lib/placement-lifecycle.ts +5 -0
- package/template/lib/row-height.ts +10 -0
- package/template/lib/stock-portrait.ts +11 -0
- package/template/lib/utils.test.ts +13 -0
- package/template/lib/utils.ts +1 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +83 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/public/Illustration/Rotation.svg +74 -0
- package/template/public/avatars/user.svg +11 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logos/exxat-one.svg +36 -0
- package/template/public/logos/exxat-prism.svg +39 -0
- package/template/public/mock-schools/emory.svg +4 -0
- package/template/public/mock-schools/rush.svg +4 -0
- package/template/scripts/fontawesome-subset-audit.mjs +190 -0
- package/template/scripts/pm2-startup-macos.sh +13 -0
- package/template/skills-lock.json +10 -0
- package/template/stores/app-store.ts +33 -0
- package/template/tests/setup.ts +1 -0
- package/template/tsconfig.json +35 -0
- package/template/types/react-payment-inputs.d.ts +19 -0
- package/template/vitest.config.ts +18 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DataTablePaginated<TData> — DataTable with a bottom pagination bar
|
|
5
|
+
*
|
|
6
|
+
* Adds:
|
|
7
|
+
* • "Rows per page" selector
|
|
8
|
+
* • First / Previous / Next / Last page buttons
|
|
9
|
+
* • "{from}–{to} of {total}" status span (role="status" aria-live="polite")
|
|
10
|
+
* • Keyboard: Left/Right arrow keys on the pagination bar change page
|
|
11
|
+
*
|
|
12
|
+
* Everything else (columns, pinning, resize, DnD, sort, filters, group,
|
|
13
|
+
* selection) is identical to DataTable — they share useTableState.
|
|
14
|
+
*
|
|
15
|
+
* Props: DataTableProps<TData> & { pagination?: PaginationConfig }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as React from "react"
|
|
19
|
+
import {
|
|
20
|
+
DropdownMenu,
|
|
21
|
+
DropdownMenuContent,
|
|
22
|
+
DropdownMenuItem,
|
|
23
|
+
DropdownMenuTrigger,
|
|
24
|
+
} from "@/components/ui/dropdown-menu"
|
|
25
|
+
import { Tip } from "@/components/ui/tip"
|
|
26
|
+
import { TooltipProvider } from "@/components/ui/tooltip"
|
|
27
|
+
import { DataTable, type DataTableExtendedProps } from "./index"
|
|
28
|
+
import type { PaginationConfig } from "./types"
|
|
29
|
+
import type { useTableState } from "./use-table-state"
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// PaginationBar
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
interface PaginationBarProps {
|
|
36
|
+
page: number
|
|
37
|
+
pageSize: number
|
|
38
|
+
total: number
|
|
39
|
+
pageSizeOptions: number[]
|
|
40
|
+
onPageChange: (p: number) => void
|
|
41
|
+
onPageSizeChange: (n: number) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function PaginationBar({
|
|
45
|
+
page,
|
|
46
|
+
pageSize,
|
|
47
|
+
total,
|
|
48
|
+
pageSizeOptions,
|
|
49
|
+
onPageChange,
|
|
50
|
+
onPageSizeChange,
|
|
51
|
+
}: PaginationBarProps) {
|
|
52
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
|
53
|
+
const from = Math.min((page - 1) * pageSize + 1, total)
|
|
54
|
+
const to = Math.min(page * pageSize, total)
|
|
55
|
+
|
|
56
|
+
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
|
57
|
+
if (e.key === "ArrowLeft" && page > 1) {
|
|
58
|
+
e.preventDefault()
|
|
59
|
+
onPageChange(page - 1)
|
|
60
|
+
} else if (e.key === "ArrowRight" && page < totalPages) {
|
|
61
|
+
e.preventDefault()
|
|
62
|
+
onPageChange(page + 1)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className="flex items-center justify-between px-4 py-2.5 border-t border-border bg-background text-sm select-none"
|
|
69
|
+
onKeyDown={handleKeyDown}
|
|
70
|
+
>
|
|
71
|
+
{/* Rows per page */}
|
|
72
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
73
|
+
<span>Rows per page</span>
|
|
74
|
+
<DropdownMenu>
|
|
75
|
+
<DropdownMenuTrigger asChild>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
aria-label="Rows per page"
|
|
79
|
+
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-input bg-background hover:bg-interactive-hover text-foreground text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
80
|
+
>
|
|
81
|
+
{pageSize}
|
|
82
|
+
<i className="fa-light fa-chevron-down text-xs" aria-hidden="true" />
|
|
83
|
+
</button>
|
|
84
|
+
</DropdownMenuTrigger>
|
|
85
|
+
<DropdownMenuContent align="start" className="w-20">
|
|
86
|
+
{pageSizeOptions.map(n => (
|
|
87
|
+
<DropdownMenuItem key={n} onClick={() => onPageSizeChange(n)}>
|
|
88
|
+
{n}
|
|
89
|
+
</DropdownMenuItem>
|
|
90
|
+
))}
|
|
91
|
+
</DropdownMenuContent>
|
|
92
|
+
</DropdownMenu>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Nav */}
|
|
96
|
+
<div className="flex items-center gap-3">
|
|
97
|
+
<span
|
|
98
|
+
role="status"
|
|
99
|
+
aria-live="polite"
|
|
100
|
+
className="text-muted-foreground tabular-nums"
|
|
101
|
+
>
|
|
102
|
+
{total === 0 ? "0 results" : `${from}–${to} of ${total}`}
|
|
103
|
+
</span>
|
|
104
|
+
<TooltipProvider>
|
|
105
|
+
<div className="flex items-center gap-1">
|
|
106
|
+
<Tip label="First page" side="top">
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
aria-label="First page"
|
|
110
|
+
disabled={page === 1}
|
|
111
|
+
onClick={() => onPageChange(1)}
|
|
112
|
+
className="inline-flex items-center justify-center size-7 rounded hover:bg-interactive-hover disabled:opacity-40 disabled:pointer-events-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
113
|
+
>
|
|
114
|
+
<i className="fa-light fa-chevrons-left text-xs" aria-hidden="true" />
|
|
115
|
+
</button>
|
|
116
|
+
</Tip>
|
|
117
|
+
<Tip label="Previous page" side="top">
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
aria-label="Previous page"
|
|
121
|
+
disabled={page === 1}
|
|
122
|
+
onClick={() => onPageChange(page - 1)}
|
|
123
|
+
className="inline-flex items-center justify-center size-7 rounded hover:bg-interactive-hover disabled:opacity-40 disabled:pointer-events-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
124
|
+
>
|
|
125
|
+
<i className="fa-light fa-chevron-left text-xs" aria-hidden="true" />
|
|
126
|
+
</button>
|
|
127
|
+
</Tip>
|
|
128
|
+
<span className="px-2 text-muted-foreground tabular-nums">
|
|
129
|
+
{page} / {totalPages}
|
|
130
|
+
</span>
|
|
131
|
+
<Tip label="Next page" side="top">
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
aria-label="Next page"
|
|
135
|
+
disabled={page >= totalPages}
|
|
136
|
+
onClick={() => onPageChange(page + 1)}
|
|
137
|
+
className="inline-flex items-center justify-center size-7 rounded hover:bg-interactive-hover disabled:opacity-40 disabled:pointer-events-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
138
|
+
>
|
|
139
|
+
<i className="fa-light fa-chevron-right text-xs" aria-hidden="true" />
|
|
140
|
+
</button>
|
|
141
|
+
</Tip>
|
|
142
|
+
<Tip label="Last page" side="top">
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
aria-label="Last page"
|
|
146
|
+
disabled={page >= totalPages}
|
|
147
|
+
onClick={() => onPageChange(totalPages)}
|
|
148
|
+
className="inline-flex items-center justify-center size-7 rounded hover:bg-interactive-hover disabled:opacity-40 disabled:pointer-events-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
149
|
+
>
|
|
150
|
+
<i className="fa-light fa-chevrons-right text-xs" aria-hidden="true" />
|
|
151
|
+
</button>
|
|
152
|
+
</Tip>
|
|
153
|
+
</div>
|
|
154
|
+
</TooltipProvider>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
// DataTablePaginated<TData>
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export interface DataTablePaginatedProps<TData extends Record<string, unknown>>
|
|
165
|
+
extends DataTableExtendedProps<TData> {
|
|
166
|
+
pagination?: PaginationConfig
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function DataTablePaginated<TData extends Record<string, unknown>>({
|
|
170
|
+
data,
|
|
171
|
+
columns,
|
|
172
|
+
pagination,
|
|
173
|
+
defaultSort,
|
|
174
|
+
...rest
|
|
175
|
+
}: DataTablePaginatedProps<TData>) {
|
|
176
|
+
const config = {
|
|
177
|
+
pageSize: pagination?.pageSize ?? 10,
|
|
178
|
+
pageSizeOptions: pagination?.pageSizeOptions ?? [10, 25, 50, 100],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const [page, setPage] = React.useState(1)
|
|
182
|
+
const [pageSize, setPageSize] = React.useState(config.pageSize)
|
|
183
|
+
|
|
184
|
+
// filteredCount: total rows after filters (driven by inner table state via CountSyncer)
|
|
185
|
+
const [filteredCount, setFilteredCount] = React.useState(data.length)
|
|
186
|
+
|
|
187
|
+
const totalPages = Math.max(1, Math.ceil(filteredCount / pageSize))
|
|
188
|
+
const safePage = Math.min(page, totalPages)
|
|
189
|
+
|
|
190
|
+
function handlePageSizeChange(n: number) {
|
|
191
|
+
setPageSize(n)
|
|
192
|
+
setPage(1)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Wrap toolbarSlot to intercept state.rows.length (a number — no circular ref)
|
|
196
|
+
const originalToolbarSlot = rest.toolbarSlot
|
|
197
|
+
const toolbarSlot = React.useCallback(
|
|
198
|
+
(state: ReturnType<typeof useTableState<TData>>) => (
|
|
199
|
+
<>
|
|
200
|
+
<CountSyncer count={state.rows.length} onSync={setFilteredCount} onReset={() => setPage(1)} />
|
|
201
|
+
{originalToolbarSlot ? originalToolbarSlot(state) : null}
|
|
202
|
+
</>
|
|
203
|
+
),
|
|
204
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
205
|
+
[originalToolbarSlot],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="flex flex-col gap-0">
|
|
210
|
+
<DataTable
|
|
211
|
+
{...rest}
|
|
212
|
+
data={data}
|
|
213
|
+
columns={columns}
|
|
214
|
+
defaultSort={defaultSort}
|
|
215
|
+
toolbarSlot={toolbarSlot}
|
|
216
|
+
paginationOverride={{ page: safePage, pageSize }}
|
|
217
|
+
hasFooter
|
|
218
|
+
/>
|
|
219
|
+
<div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
|
|
220
|
+
<PaginationBar
|
|
221
|
+
page={safePage}
|
|
222
|
+
pageSize={pageSize}
|
|
223
|
+
total={filteredCount}
|
|
224
|
+
pageSizeOptions={config.pageSizeOptions}
|
|
225
|
+
onPageChange={setPage}
|
|
226
|
+
onPageSizeChange={handlePageSizeChange}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
234
|
+
// CountSyncer — syncs filtered row count (a number) to parent without loops.
|
|
235
|
+
// Syncing a primitive avoids the circular-reference issue of syncing an array.
|
|
236
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
export function CountSyncer({
|
|
239
|
+
count,
|
|
240
|
+
onSync,
|
|
241
|
+
onReset,
|
|
242
|
+
}: {
|
|
243
|
+
count: number
|
|
244
|
+
onSync: (n: number) => void
|
|
245
|
+
onReset: () => void
|
|
246
|
+
}) {
|
|
247
|
+
const prevCount = React.useRef(count)
|
|
248
|
+
React.useLayoutEffect(() => {
|
|
249
|
+
if (prevCount.current !== count) {
|
|
250
|
+
prevCount.current = count
|
|
251
|
+
onSync(count)
|
|
252
|
+
onReset()
|
|
253
|
+
}
|
|
254
|
+
}, [count, onSync, onReset])
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Generic DataTable — shared types
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
import type * as React from "react"
|
|
8
|
+
import type {
|
|
9
|
+
ConditionalRule,
|
|
10
|
+
FilterOperator,
|
|
11
|
+
FilterTextMask,
|
|
12
|
+
} from "@/components/table-properties/types"
|
|
13
|
+
export type { ConditionalRule, FilterTextMask }
|
|
14
|
+
|
|
15
|
+
export type SortDir = "asc" | "desc"
|
|
16
|
+
|
|
17
|
+
export interface ColumnDef<TData> {
|
|
18
|
+
/** Unique key — must match a key of TData or be synthetic (e.g. "select", "actions") */
|
|
19
|
+
key: string
|
|
20
|
+
/** Header label */
|
|
21
|
+
label: string
|
|
22
|
+
/** Default width in px */
|
|
23
|
+
width?: number
|
|
24
|
+
minWidth?: number
|
|
25
|
+
/** Whether this column can be sorted */
|
|
26
|
+
sortable?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Key of TData used for sorting comparisons.
|
|
29
|
+
* If omitted but sortable=true, falls back to `key`.
|
|
30
|
+
*/
|
|
31
|
+
sortKey?: keyof TData & string
|
|
32
|
+
/** Pin to left or right by default */
|
|
33
|
+
defaultPin?: "left" | "right"
|
|
34
|
+
/** If true, user cannot unpin this column */
|
|
35
|
+
lockPin?: boolean
|
|
36
|
+
/** Render the cell content. If omitted, renders String(row[key]). */
|
|
37
|
+
cell?: (row: TData, ctx: CellContext<TData>) => React.ReactNode
|
|
38
|
+
/** Custom header renderer — overrides the default label text */
|
|
39
|
+
header?: () => React.ReactNode
|
|
40
|
+
/** Filter config — drives per-column "Filter by this column" option */
|
|
41
|
+
filter?: {
|
|
42
|
+
type: "select" | "text" | "date"
|
|
43
|
+
/** icon class for filter pills, e.g. "fa-circle-dot" */
|
|
44
|
+
icon?: string
|
|
45
|
+
options?: { value: string; label: string }[]
|
|
46
|
+
operators?: FilterOperator[]
|
|
47
|
+
/** When `type` is `text`, optional mask for filter popover + drawer. */
|
|
48
|
+
textMask?: FilterTextMask
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CellContext<TData> {
|
|
53
|
+
rowIndex: number
|
|
54
|
+
selected: boolean
|
|
55
|
+
onSelect: (selected: boolean) => void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DataTableProps<TData extends Record<string, unknown>> {
|
|
59
|
+
/** Row data */
|
|
60
|
+
data: TData[]
|
|
61
|
+
/** Column definitions */
|
|
62
|
+
columns: ColumnDef<TData>[]
|
|
63
|
+
/** Returns a stable unique ID for each row (used for selection keys) */
|
|
64
|
+
getRowId?: (row: TData, index: number) => string | number
|
|
65
|
+
/**
|
|
66
|
+
* Accessible name for each row’s selection checkbox (e.g. primary column value).
|
|
67
|
+
* If omitted, a generic label is used.
|
|
68
|
+
*/
|
|
69
|
+
getRowSelectionLabel?: (row: TData, rowIndex: number) => string
|
|
70
|
+
/** Enable row selection checkboxes */
|
|
71
|
+
selectable?: boolean
|
|
72
|
+
/** Enable global search */
|
|
73
|
+
searchable?: boolean
|
|
74
|
+
/** Enable "Group by" feature */
|
|
75
|
+
groupable?: boolean
|
|
76
|
+
/** Custom empty state */
|
|
77
|
+
emptyState?: React.ReactNode
|
|
78
|
+
/** Called when a row is clicked */
|
|
79
|
+
onRowClick?: (row: TData) => void
|
|
80
|
+
/** Default sort */
|
|
81
|
+
defaultSort?: { key: string; dir: SortDir }
|
|
82
|
+
/** Conditional formatting rules — apply bg color to cells based on value */
|
|
83
|
+
conditionalRules?: ConditionalRule[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface PaginationConfig {
|
|
87
|
+
/** Rows per page. Default 10. */
|
|
88
|
+
pageSize?: number
|
|
89
|
+
/** Options shown in the page-size selector. Default [10, 25, 50, 100]. */
|
|
90
|
+
pageSizeOptions?: number[]
|
|
91
|
+
}
|