@exxatdesignux/ui 0.0.5 → 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.
Files changed (264) hide show
  1. package/bin/init.mjs +29 -0
  2. package/package.json +7 -2
  3. package/template/.nvmrc +1 -0
  4. package/template/.prettierignore +7 -0
  5. package/template/.prettierrc +11 -0
  6. package/template/AGENTS.md +485 -0
  7. package/template/Logo/Exxat_Prism.svg +39 -0
  8. package/template/Logo/Exxat_one.svg +36 -0
  9. package/template/README.md +58 -0
  10. package/template/app/(app)/compliance/page.tsx +10 -0
  11. package/template/app/(app)/dashboard/loading.tsx +18 -0
  12. package/template/app/(app)/dashboard/page.tsx +36 -0
  13. package/template/app/(app)/data-list/[id]/page.tsx +28 -0
  14. package/template/app/(app)/data-list/new/page.tsx +31 -0
  15. package/template/app/(app)/data-list/page.tsx +10 -0
  16. package/template/app/(app)/error.tsx +43 -0
  17. package/template/app/(app)/help/page.tsx +34 -0
  18. package/template/app/(app)/layout.tsx +54 -0
  19. package/template/app/(app)/loading.tsx +18 -0
  20. package/template/app/(app)/question-bank/page.tsx +10 -0
  21. package/template/app/(app)/rotations/page.tsx +15 -0
  22. package/template/app/(app)/settings/page.tsx +17 -0
  23. package/template/app/(app)/sites/all/page.tsx +13 -0
  24. package/template/app/(app)/team/page.tsx +10 -0
  25. package/template/app/favicon.ico +0 -0
  26. package/template/app/globals.css +1811 -0
  27. package/template/app/layout.tsx +95 -0
  28. package/template/app/page.tsx +9 -0
  29. package/template/components/.gitkeep +0 -0
  30. package/template/components/app-sidebar-dynamic.tsx +15 -0
  31. package/template/components/app-sidebar.tsx +901 -0
  32. package/template/components/ask-leo-composer.tsx +216 -0
  33. package/template/components/ask-leo-sidebar.tsx +509 -0
  34. package/template/components/chart-area-interactive.tsx +293 -0
  35. package/template/components/charts-overview.tsx +2321 -0
  36. package/template/components/command-menu-01.tsx +133 -0
  37. package/template/components/command-menu-02.tsx +386 -0
  38. package/template/components/command-menu.tsx +182 -0
  39. package/template/components/compliance-board-view.tsx +134 -0
  40. package/template/components/compliance-client.tsx +92 -0
  41. package/template/components/compliance-list-view.tsx +59 -0
  42. package/template/components/compliance-page-header.tsx +89 -0
  43. package/template/components/compliance-table.tsx +525 -0
  44. package/template/components/dashboard-onboarding-gallery.tsx +13 -0
  45. package/template/components/dashboard-onboarding.tsx +21 -0
  46. package/template/components/dashboard-promo-banner.tsx +67 -0
  47. package/template/components/dashboard-quota-progress-card.tsx +369 -0
  48. package/template/components/dashboard-report-charts.tsx +69 -0
  49. package/template/components/dashboard-section-heading.tsx +68 -0
  50. package/template/components/dashboard-tabs.tsx +598 -0
  51. package/template/components/data-list-client.tsx +239 -0
  52. package/template/components/data-list-table-cells.test.tsx +22 -0
  53. package/template/components/data-list-table-cells.tsx +173 -0
  54. package/template/components/data-list-table.tsx +879 -0
  55. package/template/components/data-table/filter-date-calendar.tsx +38 -0
  56. package/template/components/data-table/filter-text-value-input.tsx +77 -0
  57. package/template/components/data-table/index.tsx +1612 -0
  58. package/template/components/data-table/pagination.tsx +256 -0
  59. package/template/components/data-table/types.ts +91 -0
  60. package/template/components/data-table/use-table-state.ts +566 -0
  61. package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
  62. package/template/components/data-view-dashboard-charts-team.tsx +968 -0
  63. package/template/components/data-view-dashboard-charts.tsx +1668 -0
  64. package/template/components/data-views/board-card-primitives.tsx +93 -0
  65. package/template/components/data-views/index.ts +41 -0
  66. package/template/components/data-views/list-page-board-card.tsx +192 -0
  67. package/template/components/data-views/list-page-board-template.tsx +122 -0
  68. package/template/components/data-views/placement-board-card.tsx +262 -0
  69. package/template/components/export-drawer.tsx +375 -0
  70. package/template/components/exxat-product-logo.tsx +453 -0
  71. package/template/components/form-layout-01.tsx +131 -0
  72. package/template/components/getting-started.tsx +625 -0
  73. package/template/components/key-metrics.tsx +920 -0
  74. package/template/components/leo-insight-indicator.tsx +364 -0
  75. package/template/components/leo-typing-dots.tsx +121 -0
  76. package/template/components/list-hub-status-badge.tsx +51 -0
  77. package/template/components/list-page-dashboard-charts.tsx +18 -0
  78. package/template/components/nav-documents.tsx +89 -0
  79. package/template/components/nav-main.tsx +58 -0
  80. package/template/components/nav-secondary.tsx +64 -0
  81. package/template/components/nav-user.tsx +190 -0
  82. package/template/components/new-placement-back-btn.tsx +28 -0
  83. package/template/components/new-placement-form.tsx +1066 -0
  84. package/template/components/onboarding/index.ts +4 -0
  85. package/template/components/onboarding/onboarding-01.tsx +7 -0
  86. package/template/components/onboarding/onboarding-02.tsx +7 -0
  87. package/template/components/onboarding/onboarding-03.tsx +7 -0
  88. package/template/components/onboarding/onboarding-04.tsx +7 -0
  89. package/template/components/page-header.tsx +57 -0
  90. package/template/components/placement-detail.tsx +438 -0
  91. package/template/components/placements-board-view.tsx +404 -0
  92. package/template/components/placements-list-view.tsx +285 -0
  93. package/template/components/placements-page-header.tsx +160 -0
  94. package/template/components/placements-table-columns.tsx +639 -0
  95. package/template/components/product-switcher.tsx +116 -0
  96. package/template/components/question-bank-board-view.tsx +205 -0
  97. package/template/components/question-bank-client.tsx +77 -0
  98. package/template/components/question-bank-list-view.tsx +59 -0
  99. package/template/components/question-bank-page-header.tsx +89 -0
  100. package/template/components/question-bank-table.tsx +586 -0
  101. package/template/components/rotations-empty-state.tsx +47 -0
  102. package/template/components/rotations-panel-activator.tsx +8 -0
  103. package/template/components/secondary-nav.tsx +394 -0
  104. package/template/components/secondary-panel.tsx +239 -0
  105. package/template/components/section-cards.tsx +106 -0
  106. package/template/components/settings-appearance-card.tsx +424 -0
  107. package/template/components/settings-client.tsx +537 -0
  108. package/template/components/settings-form-row.tsx +42 -0
  109. package/template/components/sidebar-auto-collapse.tsx +23 -0
  110. package/template/components/sidebar-auto-open.tsx +18 -0
  111. package/template/components/sidebar-shell.tsx +37 -0
  112. package/template/components/site-header.tsx +93 -0
  113. package/template/components/sites-all-client.tsx +154 -0
  114. package/template/components/sites-board-view.tsx +67 -0
  115. package/template/components/sites-list-view.tsx +47 -0
  116. package/template/components/sites-table.tsx +312 -0
  117. package/template/components/system-banner-slot.tsx +66 -0
  118. package/template/components/table-properties/column-row.tsx +90 -0
  119. package/template/components/table-properties/draggable-list.ts +49 -0
  120. package/template/components/table-properties/drawer-button.tsx +231 -0
  121. package/template/components/table-properties/drawer.tsx +1102 -0
  122. package/template/components/table-properties/filter-card.tsx +251 -0
  123. package/template/components/table-properties/index.ts +22 -0
  124. package/template/components/table-properties/sort-card.tsx +59 -0
  125. package/template/components/table-properties/types.ts +124 -0
  126. package/template/components/task-list-panel.tsx +98 -0
  127. package/template/components/task-priority-badge.tsx +28 -0
  128. package/template/components/team-board-view.tsx +114 -0
  129. package/template/components/team-client.tsx +93 -0
  130. package/template/components/team-list-view.tsx +62 -0
  131. package/template/components/team-page-header.tsx +92 -0
  132. package/template/components/team-table.tsx +525 -0
  133. package/template/components/templates/list-page.tsx +576 -0
  134. package/template/components/templates/primary-page-template.tsx +56 -0
  135. package/template/components/theme-color-sync.tsx +32 -0
  136. package/template/components/theme-provider.tsx +71 -0
  137. package/template/components/tinted-icon-disc.tsx +53 -0
  138. package/template/components/ui/ai-thinking-surface.tsx +121 -0
  139. package/template/components/ui/avatar.tsx +1 -0
  140. package/template/components/ui/badge.tsx +1 -0
  141. package/template/components/ui/banner.tsx +1 -0
  142. package/template/components/ui/breadcrumb.tsx +1 -0
  143. package/template/components/ui/button.tsx +1 -0
  144. package/template/components/ui/calendar.tsx +1 -0
  145. package/template/components/ui/card.tsx +1 -0
  146. package/template/components/ui/chart.tsx +1 -0
  147. package/template/components/ui/checkbox.tsx +1 -0
  148. package/template/components/ui/coach-mark.tsx +1 -0
  149. package/template/components/ui/collapsible.tsx +1 -0
  150. package/template/components/ui/command.tsx +1 -0
  151. package/template/components/ui/date-picker-field.tsx +1 -0
  152. package/template/components/ui/dialog.tsx +1 -0
  153. package/template/components/ui/dot-pattern.tsx +159 -0
  154. package/template/components/ui/drag-handle-grip.tsx +1 -0
  155. package/template/components/ui/drawer.tsx +1 -0
  156. package/template/components/ui/dropdown-menu.tsx +1 -0
  157. package/template/components/ui/field.tsx +1 -0
  158. package/template/components/ui/form.tsx +1 -0
  159. package/template/components/ui/input-group.tsx +1 -0
  160. package/template/components/ui/input-mask.tsx +1 -0
  161. package/template/components/ui/input.tsx +1 -0
  162. package/template/components/ui/kbd.tsx +1 -0
  163. package/template/components/ui/label.tsx +1 -0
  164. package/template/components/ui/leo-icon.tsx +726 -0
  165. package/template/components/ui/payment-card-fields.tsx +1 -0
  166. package/template/components/ui/popover.tsx +1 -0
  167. package/template/components/ui/radio-group.tsx +1 -0
  168. package/template/components/ui/select.tsx +1 -0
  169. package/template/components/ui/selection-tile-grid.tsx +1 -0
  170. package/template/components/ui/separator.tsx +1 -0
  171. package/template/components/ui/sheet.tsx +1 -0
  172. package/template/components/ui/sidebar.tsx +1 -0
  173. package/template/components/ui/skeleton.tsx +1 -0
  174. package/template/components/ui/sonner.tsx +1 -0
  175. package/template/components/ui/status-badge.tsx +1 -0
  176. package/template/components/ui/table.tsx +1 -0
  177. package/template/components/ui/tabs.tsx +1 -0
  178. package/template/components/ui/textarea.tsx +1 -0
  179. package/template/components/ui/tip.tsx +1 -0
  180. package/template/components/ui/toggle-group.tsx +1 -0
  181. package/template/components/ui/toggle-switch.tsx +1 -0
  182. package/template/components/ui/toggle.tsx +1 -0
  183. package/template/components/ui/tooltip.tsx +1 -0
  184. package/template/components/ui/view-segmented-control.tsx +1 -0
  185. package/template/components.json +27 -0
  186. package/template/contexts/chart-variant-context.tsx +35 -0
  187. package/template/contexts/command-menu-context.tsx +28 -0
  188. package/template/contexts/dashboard-view-context.tsx +35 -0
  189. package/template/contexts/product-context.tsx +38 -0
  190. package/template/contexts/system-banner-context.tsx +127 -0
  191. package/template/docs/command-menu-pattern.md +45 -0
  192. package/template/docs/data-views-pattern.md +160 -0
  193. package/template/ecosystem.config.cjs +20 -0
  194. package/template/eslint.config.mjs +18 -0
  195. package/template/fontawesome-subset.manifest.json +190 -0
  196. package/template/hooks/.gitkeep +0 -0
  197. package/template/hooks/use-app-theme.ts +1 -0
  198. package/template/hooks/use-coach-mark.ts +1 -0
  199. package/template/hooks/use-mobile.ts +1 -0
  200. package/template/hooks/use-mod-key-label.ts +1 -0
  201. package/template/lib/.gitkeep +0 -0
  202. package/template/lib/ask-leo-route-context.ts +133 -0
  203. package/template/lib/chart-keyboard-selection.test.ts +20 -0
  204. package/template/lib/chart-keyboard-selection.ts +17 -0
  205. package/template/lib/chart-line-dash.ts +16 -0
  206. package/template/lib/coach-mark-registry.ts +68 -0
  207. package/template/lib/command-menu-config.ts +127 -0
  208. package/template/lib/command-menu-search-data.ts +44 -0
  209. package/template/lib/conditional-rule-match.ts +32 -0
  210. package/template/lib/dashboard-customize-coach-mark.ts +18 -0
  211. package/template/lib/dashboard-layout-merge.ts +63 -0
  212. package/template/lib/data-list-display-options.ts +35 -0
  213. package/template/lib/data-list-persistence.ts +280 -0
  214. package/template/lib/data-list-view-surface.ts +58 -0
  215. package/template/lib/data-list-view.ts +29 -0
  216. package/template/lib/data-view-dashboard-storage.ts +101 -0
  217. package/template/lib/date-filter.ts +8 -0
  218. package/template/lib/dev-log.test.ts +28 -0
  219. package/template/lib/dev-log.ts +8 -0
  220. package/template/lib/editable-target.ts +10 -0
  221. package/template/lib/floating-sheet-panel.ts +72 -0
  222. package/template/lib/initials-from-name.ts +7 -0
  223. package/template/lib/list-page-table-properties.ts +52 -0
  224. package/template/lib/list-status-badges.ts +168 -0
  225. package/template/lib/logo-dev.ts +12 -0
  226. package/template/lib/mock/compliance-kpi.ts +61 -0
  227. package/template/lib/mock/compliance.ts +146 -0
  228. package/template/lib/mock/dashboard.ts +105 -0
  229. package/template/lib/mock/navigation.tsx +231 -0
  230. package/template/lib/mock/placements-kpi.ts +134 -0
  231. package/template/lib/mock/placements.ts +183 -0
  232. package/template/lib/mock/question-bank-kpi.ts +61 -0
  233. package/template/lib/mock/question-bank.ts +142 -0
  234. package/template/lib/mock/sites-directory.ts +16 -0
  235. package/template/lib/mock/sites-kpi.ts +25 -0
  236. package/template/lib/mock/team-kpi.ts +60 -0
  237. package/template/lib/mock/team.ts +118 -0
  238. package/template/lib/motion-ui.ts +17 -0
  239. package/template/lib/placement-board-card-layout.ts +79 -0
  240. package/template/lib/placement-lifecycle.ts +5 -0
  241. package/template/lib/row-height.ts +10 -0
  242. package/template/lib/stock-portrait.ts +11 -0
  243. package/template/lib/utils.test.ts +13 -0
  244. package/template/lib/utils.ts +1 -0
  245. package/template/next.config.mjs +15 -0
  246. package/template/package.json +83 -0
  247. package/template/postcss.config.mjs +8 -0
  248. package/template/public/.gitkeep +0 -0
  249. package/template/public/Illustration/Rotation.svg +74 -0
  250. package/template/public/avatars/user.svg +11 -0
  251. package/template/public/favicon/favicon.ico +0 -0
  252. package/template/public/favicon.ico +0 -0
  253. package/template/public/logos/exxat-one.svg +36 -0
  254. package/template/public/logos/exxat-prism.svg +39 -0
  255. package/template/public/mock-schools/emory.svg +4 -0
  256. package/template/public/mock-schools/rush.svg +4 -0
  257. package/template/scripts/fontawesome-subset-audit.mjs +190 -0
  258. package/template/scripts/pm2-startup-macos.sh +13 -0
  259. package/template/skills-lock.json +10 -0
  260. package/template/stores/app-store.ts +33 -0
  261. package/template/tests/setup.ts +1 -0
  262. package/template/tsconfig.json +35 -0
  263. package/template/types/react-payment-inputs.d.ts +19 -0
  264. 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
+ }