@exxatdesignux/ui 0.3.0 → 0.4.1

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 (214) hide show
  1. package/CHANGELOG.md +701 -6
  2. package/README.md +138 -0
  3. package/bin/init.mjs +134 -31
  4. package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
  5. package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
  6. package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
  7. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
  8. package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
  9. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
  10. package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
  11. package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
  12. package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
  13. package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
  14. package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
  15. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
  16. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
  17. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
  18. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
  19. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
  20. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
  21. package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
  22. package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
  23. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
  24. package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
  25. package/consumer-extras/handbook/HANDBOOK.md +2 -0
  26. package/consumer-extras/handbook/glossary.md +2 -1
  27. package/consumer-extras/handbook/reference-implementations.md +31 -4
  28. package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
  29. package/consumer-extras/patterns/data-views-pattern.md +18 -16
  30. package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
  31. package/dist/components/data-table/index.js +2 -2
  32. package/dist/components/data-table/index.js.map +1 -1
  33. package/dist/components/data-table/pagination.js +3 -3
  34. package/dist/components/data-table/pagination.js.map +1 -1
  35. package/dist/components/data-table/use-table-state.d.ts +1 -1
  36. package/dist/components/data-table/use-table-state.js.map +1 -1
  37. package/dist/components/data-views/data-row-list.js.map +1 -1
  38. package/dist/components/data-views/finder-panel-view.d.ts +1 -1
  39. package/dist/components/data-views/finder-panel-view.js.map +1 -1
  40. package/dist/components/data-views/hub-table.d.ts +9 -3
  41. package/dist/components/data-views/hub-table.js +262 -40
  42. package/dist/components/data-views/hub-table.js.map +1 -1
  43. package/dist/components/data-views/index.js +262 -40
  44. package/dist/components/data-views/index.js.map +1 -1
  45. package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
  46. package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
  47. package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
  48. package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
  49. package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
  50. package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
  51. package/dist/components/data-views/os-folder-glyph.js.map +1 -1
  52. package/dist/components/ui/avatar.d.ts +1 -1
  53. package/dist/components/ui/key-metrics.js.map +1 -1
  54. package/dist/index.js +136 -39
  55. package/dist/index.js.map +1 -1
  56. package/package.json +3 -2
  57. package/src/components/data-table/index.tsx +2 -2
  58. package/src/components/data-table/pagination.tsx +5 -1
  59. package/src/components/data-table/use-table-state.ts +1 -1
  60. package/src/components/data-views/data-row-list.tsx +1 -1
  61. package/src/components/data-views/finder-panel-view.tsx +2 -2
  62. package/src/components/data-views/hub-table.tsx +149 -41
  63. package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
  64. package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
  65. package/src/components/data-views/os-folder-glyph.tsx +1 -1
  66. package/src/components/ui/key-metrics.tsx +1 -1
  67. package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
  68. package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
  69. package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
  70. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
  71. package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
  72. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
  73. package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
  74. package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
  75. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
  76. package/template/AGENTS.md +43 -37
  77. package/template/app/(app)/columns/page.tsx +11 -0
  78. package/template/app/(app)/library/all/page.tsx +11 -0
  79. package/template/app/(app)/library/find/page.tsx +12 -0
  80. package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
  81. package/template/app/(app)/library/list/page.tsx +12 -0
  82. package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
  83. package/template/app/(app)/library/page.tsx +11 -0
  84. package/template/app/(app)/tokens-themes/page.tsx +11 -0
  85. package/template/components/ask-leo-composer.tsx +2 -2
  86. package/template/components/columns-client.tsx +158 -0
  87. package/template/components/columns-showcase.tsx +541 -0
  88. package/template/components/data-views/index.ts +32 -6
  89. package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
  90. package/template/components/data-views/table-cells.tsx +673 -0
  91. package/template/components/folder-details-shell.tsx +11 -11
  92. package/template/components/hub-tree-panel-view.tsx +24 -24
  93. package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
  94. package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
  95. package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
  96. package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
  97. package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
  98. package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
  99. package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
  100. package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
  101. package/template/components/library-panel-activator.tsx +8 -0
  102. package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
  103. package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
  104. package/template/components/list-hub-status-badge.tsx +2 -2
  105. package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
  106. package/template/components/sidebar/app-sidebar.tsx +61 -5
  107. package/template/components/sidebar/secondary-panel.tsx +109 -56
  108. package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
  109. package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
  110. package/template/components/table-properties/types.ts +1 -1
  111. package/template/components/templates/discovery-hub-template.tsx +1 -1
  112. package/template/components/templates/new-focus-template.tsx +2 -2
  113. package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
  114. package/template/components/tokens-secondary-nav.tsx +192 -0
  115. package/template/components/tokens-themes-client.tsx +476 -0
  116. package/template/components/tokens-themes-section.tsx +386 -0
  117. package/template/docs/HANDBOOK.md +187 -0
  118. package/template/docs/blueprints/README.md +1 -1
  119. package/template/docs/blueprints/board-card.md +1 -1
  120. package/template/docs/blueprints/data-table.md +2 -2
  121. package/template/docs/blueprints/list-page-template.md +3 -3
  122. package/template/docs/blueprints/page-header.md +4 -4
  123. package/template/docs/collaboration-access-pattern.md +7 -7
  124. package/template/docs/component-selection-guide.md +1 -1
  125. package/template/docs/data-views-pattern.md +18 -16
  126. package/template/docs/glossary.md +58 -0
  127. package/template/docs/kpi-flat-band-pattern.md +3 -3
  128. package/template/docs/kpi-trend-pattern.md +18 -3
  129. package/template/docs/large-dataset-strategy.md +155 -0
  130. package/template/docs/library-hub-header-pattern.md +25 -0
  131. package/template/docs/migrations/_template.md +1 -1
  132. package/template/docs/reference-implementations.md +151 -0
  133. package/template/docs/token-taxonomy.md +1 -1
  134. package/template/docs/voice-and-tone.md +262 -0
  135. package/template/eslint.config.mjs +9 -39
  136. package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
  137. package/template/lib/ask-leo-route-context.ts +6 -18
  138. package/template/lib/coach-mark-registry.ts +0 -16
  139. package/template/lib/command-menu-config.ts +5 -12
  140. package/template/lib/command-menu-search-data.ts +8 -39
  141. package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
  142. package/template/lib/library-dedicated-search.ts +19 -0
  143. package/template/lib/library-hub-search.ts +90 -0
  144. package/template/lib/library-nav.ts +477 -0
  145. package/template/lib/library-recent-searches.ts +22 -0
  146. package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
  147. package/template/lib/list-status-badges.ts +16 -104
  148. package/template/lib/mock/dashboard.ts +1 -1
  149. package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
  150. package/template/lib/mock/library-header-collaborators.ts +54 -0
  151. package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
  152. package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
  153. package/template/lib/mock/library.ts +249 -0
  154. package/template/lib/mock/navigation.tsx +32 -26
  155. package/template/lib/table-state-lifecycle.ts +1 -1
  156. package/template/next.config.mjs +7 -4
  157. package/template/package.json +0 -1
  158. package/tokens/hooks-index.json +2874 -0
  159. package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
  160. package/template/app/(app)/examples/page.tsx +0 -41
  161. package/template/app/(app)/question-bank/find/page.tsx +0 -12
  162. package/template/app/(app)/question-bank/library/page.tsx +0 -11
  163. package/template/app/(app)/question-bank/list/page.tsx +0 -12
  164. package/template/app/(app)/question-bank/page.tsx +0 -11
  165. package/template/components/compliance-board-view.tsx +0 -142
  166. package/template/components/compliance-client.tsx +0 -92
  167. package/template/components/compliance-page-header.tsx +0 -89
  168. package/template/components/compliance-table.tsx +0 -468
  169. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  170. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  171. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  172. package/template/components/new-placement-back-btn.tsx +0 -28
  173. package/template/components/new-placement-form.tsx +0 -942
  174. package/template/components/placement-board-card.tsx +0 -250
  175. package/template/components/placement-detail.tsx +0 -438
  176. package/template/components/placements-board-view.tsx +0 -397
  177. package/template/components/placements-client.tsx +0 -220
  178. package/template/components/placements-list-view.tsx +0 -124
  179. package/template/components/placements-page-header.tsx +0 -166
  180. package/template/components/placements-table-cells.test.tsx +0 -22
  181. package/template/components/placements-table-cells.tsx +0 -173
  182. package/template/components/placements-table-columns.tsx +0 -210
  183. package/template/components/placements-table.tsx +0 -934
  184. package/template/components/question-bank-panel-activator.tsx +0 -8
  185. package/template/components/rotations-empty-state.tsx +0 -50
  186. package/template/components/rotations-panel-activator.tsx +0 -8
  187. package/template/components/sites-board-view.tsx +0 -67
  188. package/template/components/sites-client.tsx +0 -154
  189. package/template/components/sites-table.tsx +0 -249
  190. package/template/components/team-board-view.tsx +0 -122
  191. package/template/components/team-client.tsx +0 -100
  192. package/template/components/team-page-header.tsx +0 -92
  193. package/template/components/team-table.tsx +0 -553
  194. package/template/docs/question-bank-hub-header-pattern.md +0 -25
  195. package/template/lib/compliance-supported-views.ts +0 -10
  196. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  197. package/template/lib/mock/compliance-kpi.ts +0 -61
  198. package/template/lib/mock/compliance.ts +0 -146
  199. package/template/lib/mock/placements-kpi.ts +0 -134
  200. package/template/lib/mock/placements.ts +0 -176
  201. package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
  202. package/template/lib/mock/question-bank.ts +0 -249
  203. package/template/lib/mock/sites-directory.ts +0 -16
  204. package/template/lib/mock/sites-kpi.ts +0 -25
  205. package/template/lib/mock/team-kpi.ts +0 -60
  206. package/template/lib/mock/team.ts +0 -118
  207. package/template/lib/placement-board-card-layout.ts +0 -79
  208. package/template/lib/question-bank-dedicated-search.ts +0 -19
  209. package/template/lib/question-bank-hub-search.ts +0 -90
  210. package/template/lib/question-bank-nav.ts +0 -477
  211. package/template/lib/question-bank-recent-searches.ts +0 -22
  212. package/template/lib/question-bank-supported-views.ts +0 -12
  213. package/template/lib/sites-supported-views.ts +0 -10
  214. package/template/lib/team-supported-views.ts +0 -10
@@ -1,934 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * PlacementsTable — placements hub composed on top of the centralized `<HubTable>`. Owns:
5
- * placement-specific column defs / cells, board/list/folder/tree/panel/dashboard renderers,
6
- * pagination chrome wrapping the table + list views, and the dashboard layout state.
7
- *
8
- * Single dataset rule: `HubTable` runs one `useTableState(rows, columns, …)`. Every renderer
9
- * (board, list, folder, tree, panel, dashboard) reads `state.rows`/`state.pagedRows` — the same
10
- * filtered/sorted/paged bag as the grid.
11
- *
12
- * View tabs drive `view` (table | list | board | …). One canonical column set + row bag —
13
- * lifecycle segmentation has been removed; every tab sees the same placements.
14
- */
15
-
16
- import * as React from "react"
17
- import dynamic from "next/dynamic"
18
- import { useRouter } from "next/navigation"
19
- import { cn } from "@/lib/utils"
20
- import { mailtoHref } from "@/lib/mailto"
21
- import { Button } from "@/components/ui/button"
22
- import { Tip } from "@/components/ui/tip"
23
- import { Skeleton } from "@/components/ui/skeleton"
24
- import { AvatarInitials } from "@/components/ui/avatar"
25
- import { CoachMark } from "@/components/ui/coach-mark"
26
- import { useCoachMark } from "@/hooks/use-coach-mark"
27
- import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
28
- import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
29
- import {
30
- ALL_DASHBOARD_CARDS,
31
- DEFAULT_VISIBLE_CARDS,
32
- DEFAULT_SPANS,
33
- DEFAULT_CHART_TYPES,
34
- loadDashboardLayout,
35
- mergeDashboardLayout,
36
- saveDashboardLayout,
37
- type ChartType,
38
- type DashboardLayout,
39
- } from "@/lib/data-view-dashboard-placements-layout"
40
- import { PlacementsBoardView, type PlacementsBoardColumnMenu } from "@/components/placements-board-view"
41
- import {
42
- PlacementListRowContent,
43
- PLACEMENT_LIST_ESTIMATE_ROW_PX,
44
- PLACEMENT_LIST_VIRTUAL_ROWS_THRESHOLD,
45
- } from "@/components/placements-list-view"
46
- import {
47
- FolderGridView,
48
- ListPageTreePanelShell,
49
- HubTable,
50
- type HubTableHandle,
51
- type HubTableRenderers,
52
- type HubTableRendererArgs,
53
- } from "@/components/data-views"
54
- import { DataRowList } from "@/components/data-views/data-row-list"
55
- import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
56
- import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
57
- import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
58
- import { ListPageSplitDetailsPlaceholder } from "@/components/data-views/list-page-split-details-placeholder"
59
- import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
60
- import { isBoardFieldActive } from "@/lib/placement-board-card-layout"
61
- import { TablePropertiesDrawerButton } from "@/components/table-properties"
62
- import type { DataListViewType } from "@/lib/data-list-view"
63
- import {
64
- DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
65
- type DataListDisplayOptions,
66
- } from "@/lib/data-list-display-options"
67
- import { StatusBadge } from "@/components/placements-table-cells"
68
- import { DataTable, DataTableToolbar } from "@/components/data-table"
69
- import { CountSyncer, PaginationBar } from "@/components/data-table/pagination"
70
- import type { ColumnDef, ConditionalRule } from "@/components/data-table/types"
71
- import { useTableState } from "@/components/data-table/use-table-state"
72
- import { ALL_PLACEMENTS, type Placement, type Status } from "@/lib/mock/placements"
73
- import {
74
- getPlacementColumns,
75
- PLACEMENT_DRAWER_LABEL,
76
- PLACEMENT_EMPTY_COPY,
77
- } from "@/components/placements-table-columns"
78
- import { placementKpiInsightFromRows, placementKpiMetricsFromRows } from "@/lib/mock/placements-kpi"
79
- import { PLACEMENTS_SUPPORTED_VIEWS } from "@/lib/placements-supported-views"
80
-
81
- // ─── Dynamic dashboard charts section (heavy; loaded only on dashboard tab) ──
82
-
83
- const PlacementsDashboardChartsSection = dynamic(
84
- () =>
85
- import("@/components/data-view-dashboard-charts").then(mod => ({
86
- default: mod.PlacementsDashboardChartsSection,
87
- })),
88
- {
89
- ssr: false,
90
- loading: () => (
91
- <div className="mx-4 mb-8 mt-2 flex flex-col gap-3 border border-border rounded-xl p-6 lg:mx-6">
92
- <Skeleton className="h-7 w-48 max-w-full" />
93
- <Skeleton className="min-h-[200px] w-full rounded-lg" />
94
- <Skeleton className="min-h-[200px] w-full rounded-lg" />
95
- </div>
96
- ),
97
- },
98
- )
99
-
100
- // ─── Placement-specific tile for FolderGridView ──────────────────────────────
101
-
102
- function PlacementFolderTile({
103
- row,
104
- hiddenColKeys,
105
- boardColumns,
106
- conditionalRules,
107
- onClick,
108
- }: {
109
- row: Placement
110
- hiddenColKeys: Set<string>
111
- boardColumns: ColumnDef<Placement>[]
112
- conditionalRules?: ConditionalRule[]
113
- onClick: () => void
114
- }) {
115
- const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
116
- const showStudent = isBoardFieldActive("student", hiddenColKeys, boardColumns)
117
- const showStatus = isBoardFieldActive("status", hiddenColKeys, boardColumns)
118
- const showSite = isBoardFieldActive("site", hiddenColKeys, boardColumns)
119
- const showSpec = isBoardFieldActive("specialization", hiddenColKeys, boardColumns)
120
- const showProgram = isBoardFieldActive("program", hiddenColKeys, boardColumns)
121
- const name = showStudent ? row.student : `Placement ${row.id}`
122
-
123
- const statusDotClass: Record<Status, string> = {
124
- confirmed: "bg-success",
125
- pending: "bg-warning",
126
- "under-review": "bg-brand",
127
- completed: "bg-muted-foreground",
128
- rejected: "bg-destructive",
129
- }
130
-
131
- return (
132
- <button
133
- type="button"
134
- onClick={onClick}
135
- className={`group relative flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-left hover:border-interactive-hover hover:bg-interactive-hover/30 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-all duration-100 cursor-pointer select-none w-full ${ruleBg}`}
136
- aria-label={`Open ${name}`}
137
- >
138
- <div className="relative">
139
- <AvatarInitials initials={row.initials} className="size-14 rounded-full text-lg font-semibold" />
140
- {showStatus && (
141
- <span className="absolute -bottom-0.5 -right-1 flex size-4 items-center justify-center rounded-full bg-card ring-2 ring-card" aria-hidden="true">
142
- <span className={`size-2.5 rounded-full ${statusDotClass[row.status]}`} />
143
- </span>
144
- )}
145
- </div>
146
- <p className="w-full text-center text-[13px] font-medium text-foreground leading-tight line-clamp-2">{name}</p>
147
- {showStatus && <StatusBadge status={row.status} />}
148
- {(showSite || showSpec || showProgram) && (
149
- <div className="flex w-full flex-col gap-0.5">
150
- {showSite && (
151
- <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">
152
- <i className="fa-light fa-building me-1" aria-hidden="true" />{row.site}
153
- </p>
154
- )}
155
- {showSpec && <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">{row.specialization}</p>}
156
- {showProgram && <p className="truncate text-center text-[11px] text-muted-foreground leading-tight">{row.program}</p>}
157
- </div>
158
- )}
159
- </button>
160
- )
161
- }
162
-
163
- // ─── Placement-specific list row for FinderPanelView ─────────────────────────
164
-
165
- function PlacementFinderListRow({
166
- row,
167
- isSelected,
168
- hiddenColKeys,
169
- boardColumns,
170
- conditionalRules,
171
- }: {
172
- row: Placement
173
- isSelected: boolean
174
- hiddenColKeys: Set<string>
175
- boardColumns: ColumnDef<Placement>[]
176
- conditionalRules?: ConditionalRule[]
177
- }) {
178
- const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
179
- const showStudent = isBoardFieldActive("student", hiddenColKeys, boardColumns)
180
- const showSite = isBoardFieldActive("site", hiddenColKeys, boardColumns)
181
- const name = showStudent ? row.student : `Placement ${row.id}`
182
- return (
183
- <div
184
- className={`flex w-full min-w-0 items-center gap-3 transition-colors duration-75 ${
185
- isSelected ? "bg-transparent text-accent-foreground" : cn("text-foreground", ruleBg)
186
- }`}
187
- >
188
- <AvatarInitials
189
- initials={row.initials}
190
- className={cn(
191
- "size-8 shrink-0 rounded-full text-[11px] font-semibold",
192
- isSelected ? "ring-2 ring-accent-foreground/35" : "",
193
- )}
194
- />
195
- <div className="min-w-0 flex-1">
196
- <p className={cn("truncate text-[13px] font-medium leading-tight", isSelected ? "text-accent-foreground" : "text-foreground")}>
197
- {name}
198
- </p>
199
- {showSite && (
200
- <p className={cn("mt-0.5 truncate text-[11px] leading-tight", isSelected ? "text-accent-foreground/80" : "text-muted-foreground")}>
201
- {row.site}
202
- </p>
203
- )}
204
- </div>
205
- {!isSelected && <StatusBadge status={row.status} />}
206
- </div>
207
- )
208
- }
209
-
210
- // ─── Placement-specific detail pane for FinderPanelView ──────────────────────
211
-
212
- function PlacementFinderDetail({
213
- row,
214
- hiddenColKeys,
215
- boardColumns,
216
- }: {
217
- row: Placement
218
- hiddenColKeys: Set<string>
219
- boardColumns: ColumnDef<Placement>[]
220
- }) {
221
- const router = useRouter()
222
- const show = (key: string) => isBoardFieldActive(key, hiddenColKeys, boardColumns)
223
- return (
224
- <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
225
- <div className="flex shrink-0 items-start gap-4 border-b border-border px-5 py-4">
226
- <AvatarInitials initials={row.initials} className="size-14 shrink-0 rounded-full text-lg font-semibold" />
227
- <div className="min-w-0 flex-1">
228
- <h2 className="text-base font-semibold text-foreground leading-tight">{row.student}</h2>
229
- {show("program") && <p className="mt-0.5 text-[13px] text-muted-foreground">{row.program}</p>}
230
- {show("status") && <div className="mt-2"><StatusBadge status={row.status} /></div>}
231
- </div>
232
- <Tip side="bottom" label="Open full detail page">
233
- <Button
234
- type="button"
235
- variant="outline"
236
- size="sm"
237
- className="shrink-0"
238
- onClick={() => router.push(`/data-list/${row.id}`)}
239
- aria-label={`Open full detail for ${row.student}`}
240
- >
241
- <i className="fa-light fa-arrow-up-right-from-square text-[12px]" aria-hidden="true" />
242
- Open
243
- </Button>
244
- </Tip>
245
- </div>
246
- <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
247
- <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
248
- {show("email") && (
249
- <div className="flex flex-col gap-0.5">
250
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
251
- <i className="fa-light fa-envelope text-[10px]" aria-hidden="true" /> Email
252
- </dt>
253
- <dd className="text-[13px]">
254
- <a href={mailtoHref(row.email)} className="text-interactive-foreground hover:underline">{row.email}</a>
255
- </dd>
256
- </div>
257
- )}
258
- {show("site") && (
259
- <div className="flex flex-col gap-0.5">
260
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
261
- <i className="fa-light fa-building text-[10px]" aria-hidden="true" /> Site
262
- </dt>
263
- <dd className="text-[13px] text-foreground">{row.site}</dd>
264
- </div>
265
- )}
266
- {show("internship") && (
267
- <div className="flex flex-col gap-0.5">
268
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
269
- <i className="fa-light fa-briefcase text-[10px]" aria-hidden="true" /> Internship
270
- </dt>
271
- <dd className="text-[13px] text-foreground">{row.internship}</dd>
272
- </div>
273
- )}
274
- {show("specialization") && (
275
- <div className="flex flex-col gap-0.5">
276
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
277
- <i className="fa-light fa-stethoscope text-[10px]" aria-hidden="true" /> Specialization
278
- </dt>
279
- <dd className="text-[13px] text-foreground">{row.specialization}</dd>
280
- </div>
281
- )}
282
- {show("supervisor") && (
283
- <div className="flex flex-col gap-0.5">
284
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
285
- <i className="fa-light fa-user-tie text-[10px]" aria-hidden="true" /> Supervisor
286
- </dt>
287
- <dd className="text-[13px] text-foreground">{row.supervisor}</dd>
288
- </div>
289
- )}
290
- {show("start") && (
291
- <div className="flex flex-col gap-0.5">
292
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
293
- <i className="fa-light fa-calendar text-[10px]" aria-hidden="true" /> Start Date
294
- </dt>
295
- <dd className="text-[13px] text-foreground">{row.start}</dd>
296
- </div>
297
- )}
298
- {show("duration") && (
299
- <div className="flex flex-col gap-0.5">
300
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
301
- <i className="fa-light fa-clock text-[10px]" aria-hidden="true" /> Duration
302
- </dt>
303
- <dd className="text-[13px] text-foreground">{row.duration}</dd>
304
- </div>
305
- )}
306
- {row.placementPhase === "ongoing" && (
307
- <div className="flex flex-col gap-0.5 sm:col-span-2">
308
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
309
- <i className="fa-light fa-chart-line text-[10px]" aria-hidden="true" /> Progress
310
- </dt>
311
- <dd className="text-[13px] text-foreground flex flex-col gap-1.5">
312
- <span>{row.progressWeeksDone} / {row.progressWeeksTotal} weeks</span>
313
- <div role="progressbar" aria-valuenow={row.progressWeeksDone} aria-valuemin={0} aria-valuemax={row.progressWeeksTotal}
314
- aria-label={`${row.progressWeeksDone} of ${row.progressWeeksTotal} weeks completed`}
315
- className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
316
- <div className="h-full rounded-full bg-primary transition-all"
317
- style={{ width: `${Math.round((row.progressWeeksDone / Math.max(1, row.progressWeeksTotal)) * 100)}%` }} />
318
- </div>
319
- </dd>
320
- </div>
321
- )}
322
- {row.siteAddress && (
323
- <div className="flex flex-col gap-0.5 sm:col-span-2">
324
- <dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
325
- <i className="fa-light fa-location-dot text-[10px]" aria-hidden="true" /> Site Address
326
- </dt>
327
- <dd className="text-[13px] text-foreground">{row.siteAddress}</dd>
328
- </div>
329
- )}
330
- </dl>
331
- </div>
332
- </div>
333
- )
334
- }
335
-
336
- // ─── Status groups for FinderPanelView ───────────────────────────────────────
337
-
338
- const STATUS_GROUPS: Array<{ id: Status | "all"; label: string; accent: string }> = [
339
- { id: "all", label: "All", accent: "bg-muted-foreground" },
340
- { id: "confirmed", label: "Confirmed", accent: "bg-success" },
341
- { id: "pending", label: "Pending", accent: "bg-warning" },
342
- { id: "under-review", label: "Under Review", accent: "bg-brand" },
343
- { id: "rejected", label: "Rejected", accent: "bg-destructive" },
344
- { id: "completed", label: "Completed", accent: "bg-muted-foreground/50" },
345
- ]
346
-
347
- function buildStatusGroups(rows: Placement[]): FinderGroup[] {
348
- return STATUS_GROUPS.map(sg => ({
349
- id: sg.id,
350
- label: sg.label,
351
- accent: sg.accent,
352
- count: sg.id === "all" ? rows.length : rows.filter(r => r.status === sg.id).length,
353
- }))
354
- }
355
-
356
- // ─── Tree-view body (its own selection state) ────────────────────────────────
357
-
358
- function PlacementsTreeBody({
359
- args,
360
- }: {
361
- args: HubTableRendererArgs<Placement>
362
- }) {
363
- const { state } = args
364
- const listRows = state.rows as Placement[]
365
- const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
366
- const [selectedId, setSelectedId] = React.useState<number | null>(() => listRows[0]?.id ?? null)
367
-
368
- React.useEffect(() => {
369
- if (selectedId == null) {
370
- setSelectedId(listRows[0]?.id ?? null)
371
- return
372
- }
373
- if (!listRows.some(r => r.id === selectedId)) {
374
- setSelectedId(listRows[0]?.id ?? null)
375
- }
376
- }, [listRows, selectedId])
377
-
378
- const selected = listRows.find(r => r.id === selectedId) ?? null
379
-
380
- return (
381
- <ListPageTreePanelShell
382
- resizableGroupId="data-list-tree"
383
- ariaLabel="Record outline and details"
384
- tree={
385
- <div className="flex min-h-0 flex-1 flex-col">
386
- <ListPageTreeColumnHeader title="Records" />
387
- {listRows.length === 0 ? (
388
- <p className="p-3 text-sm text-muted-foreground">{PLACEMENT_EMPTY_COPY}</p>
389
- ) : (
390
- <ul
391
- role="tree"
392
- aria-label="Demo records"
393
- className="min-h-0 flex-1 list-none space-y-0.5 overflow-y-auto py-1"
394
- >
395
- {listRows.map(row => {
396
- const isSel = selectedId === row.id
397
- return (
398
- <li key={row.id} role="none" className="py-0.5">
399
- <button
400
- type="button"
401
- role="treeitem"
402
- aria-selected={isSel}
403
- tabIndex={isSel ? 0 : -1}
404
- onClick={() => setSelectedId(row.id)}
405
- className={cn(
406
- "flex w-full min-h-8 items-center rounded-md px-3 py-2 text-left text-sm transition-colors duration-75",
407
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
408
- isSel
409
- ? "bg-accent font-medium text-accent-foreground"
410
- : "text-foreground hover:bg-muted/50",
411
- )}
412
- >
413
- <span className="min-w-0 truncate">{row.student}</span>
414
- </button>
415
- </li>
416
- )
417
- })}
418
- </ul>
419
- )}
420
- </div>
421
- }
422
- details={
423
- selected ? (
424
- <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-card">
425
- <ListPageTreeColumnHeader title="Details" />
426
- <div className="min-h-0 flex-1 overflow-y-auto">
427
- <PlacementFinderDetail
428
- row={selected}
429
- hiddenColKeys={state.hiddenCols}
430
- boardColumns={boardColumns}
431
- />
432
- </div>
433
- </div>
434
- ) : (
435
- <ListPageSplitDetailsPlaceholder title="Nothing selected" />
436
- )
437
- }
438
- />
439
- )
440
- }
441
-
442
- // ─── Dashboard body (its own layout state) ───────────────────────────────────
443
-
444
- function PlacementsDashboardBody({
445
- args,
446
- columns,
447
- }: {
448
- args: HubTableRendererArgs<Placement>
449
- columns: ColumnDef<Placement>[]
450
- }) {
451
- const { state, drawerToolbarProps, displayOptions } = args
452
- const rows = state.rows as Placement[]
453
-
454
- const dashboardKpi = React.useMemo(
455
- () => ({
456
- metrics: placementKpiMetricsFromRows(rows),
457
- insight: placementKpiInsightFromRows(rows),
458
- }),
459
- [rows],
460
- )
461
-
462
- const [visibleCards, setVisibleCards] = React.useState<string[]>(DEFAULT_VISIBLE_CARDS)
463
- const [cardOrder, setCardOrder] = React.useState<string[]>(ALL_DASHBOARD_CARDS.map(c => c.id))
464
- const [cardSpans, setCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({ ...DEFAULT_SPANS }))
465
- const [cardChartTypes, setCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({ ...DEFAULT_CHART_TYPES }))
466
- const [keyMetricsKpiCount, setKeyMetricsKpiCount] = React.useState<number>(KEY_METRICS_KPI_COUNT_DEFAULT)
467
- const [layoutEdit, setLayoutEdit] = React.useState(false)
468
- const hydrated = React.useRef(false)
469
- const baselineRef = React.useRef<DashboardLayout | null>(null)
470
-
471
- React.useEffect(() => {
472
- const saved = loadDashboardLayout()
473
- const m = mergeDashboardLayout(saved)
474
- setVisibleCards(m.visible)
475
- setCardOrder(m.order)
476
- setCardSpans(m.spans ?? { ...DEFAULT_SPANS })
477
- setCardChartTypes(m.chartTypes ?? { ...DEFAULT_CHART_TYPES })
478
- setKeyMetricsKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
479
- hydrated.current = true
480
- }, [])
481
-
482
- React.useEffect(() => {
483
- if (!hydrated.current) return
484
- saveDashboardLayout({
485
- visible: visibleCards,
486
- order: cardOrder,
487
- spans: cardSpans,
488
- chartTypes: cardChartTypes,
489
- keyMetricsKpiCount,
490
- })
491
- }, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
492
-
493
- const onResetLayout = React.useCallback(() => {
494
- setVisibleCards(ALL_DASHBOARD_CARDS.map(c => c.id))
495
- setCardOrder(ALL_DASHBOARD_CARDS.map(c => c.id))
496
- setCardSpans({ ...DEFAULT_SPANS })
497
- setCardChartTypes({ ...DEFAULT_CHART_TYPES })
498
- setKeyMetricsKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
499
- }, [])
500
-
501
- const onLayoutEditStart = React.useCallback(() => {
502
- baselineRef.current = {
503
- visible: [...visibleCards],
504
- order: [...cardOrder],
505
- spans: { ...cardSpans },
506
- chartTypes: { ...cardChartTypes },
507
- keyMetricsKpiCount,
508
- }
509
- setLayoutEdit(true)
510
- }, [visibleCards, cardOrder, cardSpans, cardChartTypes, keyMetricsKpiCount])
511
-
512
- const onLayoutEditCancel = React.useCallback(() => {
513
- const b = baselineRef.current
514
- if (b) {
515
- setVisibleCards(b.visible)
516
- setCardOrder(b.order)
517
- setCardSpans(b.spans ?? { ...DEFAULT_SPANS })
518
- setCardChartTypes(b.chartTypes ?? { ...DEFAULT_CHART_TYPES })
519
- setKeyMetricsKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
520
- }
521
- setLayoutEdit(false)
522
- }, [])
523
-
524
- const coach = useCoachMark({
525
- flowId: "data-list-dashboard-customize",
526
- steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
527
- delay: 700,
528
- dependsOnDismissedFlowId: "data-list-views-tour",
529
- })
530
-
531
- return (
532
- <>
533
- <CoachMark state={coach} />
534
- {!layoutEdit ? (
535
- <DataTableToolbar
536
- state={state}
537
- columns={columns}
538
- searchable={displayOptions.showToolbarSearch}
539
- renderFilterOptionValue={drawerToolbarProps.renderFilterOptionValue}
540
- searchAriaLabel="Search rows"
541
- toolbarSlot={s => (
542
- <TablePropertiesDrawerButton
543
- {...drawerToolbarProps}
544
- state={s}
545
- extraActions={
546
- <Tip side="bottom" label="Edit dashboard layout on canvas">
547
- <Button
548
- type="button"
549
- variant="ghost"
550
- size="icon-sm"
551
- aria-label="Edit dashboard layout"
552
- onClick={onLayoutEditStart}
553
- className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
554
- >
555
- <i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
556
- </Button>
557
- </Tip>
558
- }
559
- />
560
- )}
561
- />
562
- ) : null}
563
- <PlacementsDashboardChartsSection
564
- placements={rows}
565
- keyMetrics={dashboardKpi}
566
- visibleCards={visibleCards}
567
- cardOrder={cardOrder}
568
- cardSpans={cardSpans}
569
- cardChartTypes={cardChartTypes}
570
- keyMetricsKpiCount={keyMetricsKpiCount}
571
- layoutEditMode={layoutEdit}
572
- onVisibleChange={setVisibleCards}
573
- onOrderChange={setCardOrder}
574
- onSpanChange={(id, span) => setCardSpans(prev => ({ ...prev, [id]: span }))}
575
- onChartTypeChange={(id, t) => setCardChartTypes(prev => ({ ...prev, [id]: t }))}
576
- onKeyMetricsKpiCountChange={setKeyMetricsKpiCount}
577
- onResetLayout={onResetLayout}
578
- onLayoutEditDone={() => setLayoutEdit(false)}
579
- onLayoutEditCancel={onLayoutEditCancel}
580
- />
581
- </>
582
- )
583
- }
584
-
585
- // ─── Board renderer body ─────────────────────────────────────────────────────
586
-
587
- function PlacementsBoardBody({
588
- args,
589
- }: {
590
- args: HubTableRendererArgs<Placement>
591
- }) {
592
- const { state, displayOptions, drawerToolbarProps } = args
593
- const columns = state.displayCols
594
- const boardColumnMenu: PlacementsBoardColumnMenu = React.useMemo(
595
- () => ({
596
- filterableColumns: columns.filter(c => c.filter).map(c => ({ key: c.key, label: c.label })),
597
- sortableColumns: columns.filter(c => c.sortable && c.sortKey).map(c => ({ key: c.key, label: c.label })),
598
- groupableColumns: columns.filter(c => c.key !== "select" && c.key !== "actions").map(c => ({ key: c.key, label: c.label })),
599
- groupBy: state.groupBy,
600
- onAddFilter: state.addFilter,
601
- onSortByField: (fieldKey, direction) => {
602
- state.setSortRules(prev => {
603
- const filtered = prev.filter(r => r.fieldKey !== fieldKey)
604
- return [{ id: `sort-${Date.now()}`, fieldKey, direction }, ...filtered]
605
- })
606
- },
607
- onToggleGroupBy: (fieldKey: string) => {
608
- state.setGroupBy(prev => (prev === fieldKey ? null : fieldKey))
609
- },
610
- onOpenProperties: () => state.setSheetOpen(true),
611
- }),
612
- [columns, state],
613
- )
614
-
615
- return (
616
- <PlacementsBoardView
617
- placements={state.rows as Placement[]}
618
- boardColumnMenu={boardColumnMenu}
619
- boardDisplay={{
620
- lineCount: displayOptions.boardLineCount,
621
- showColumnLabels: displayOptions.showColumnLabels,
622
- showColumnCounts: displayOptions.showBoardColumnCounts,
623
- newCardAbove: displayOptions.boardNewCardAbove,
624
- }}
625
- hiddenColKeys={state.hiddenCols}
626
- conditionalRules={drawerToolbarProps.conditionalRules}
627
- boardColumns={state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")}
628
- />
629
- )
630
- }
631
-
632
- // ─── Props ───────────────────────────────────────────────────────────────────
633
-
634
- export interface PlacementsTableProps {
635
- view?: DataListViewType
636
- onViewChange?: (view: DataListViewType) => void
637
- /** Shared display options (persist at page level — all view types). */
638
- displayOptions?: DataListDisplayOptions
639
- onDisplayOptionsChange?: (patch: Partial<DataListDisplayOptions>) => void
640
- /** Panel view: custom groups builder. If not provided, uses default placement status groups. */
641
- panelGroupsBuilder?: (rows: Placement[]) => FinderGroup[]
642
- /** Panel view: custom list row renderer. If not provided, uses default placement row rendering. */
643
- panelRenderListRow?: (row: Placement, isSelected: boolean) => React.ReactNode
644
- /** Panel view: custom detail pane renderer. If not provided, uses default placement detail rendering. */
645
- panelRenderDetail?: (row: Placement) => React.ReactNode
646
- }
647
-
648
- /** Imperative handle — open Table Properties (table view only). */
649
- export type PlacementsTableHandle = HubTableHandle
650
-
651
- // ─── Public component ───────────────────────────────────────────────────────
652
-
653
- export const PlacementsTable = React.forwardRef<PlacementsTableHandle, PlacementsTableProps>(function PlacementsTable(
654
- {
655
- view = "table",
656
- onViewChange,
657
- displayOptions: displayOptionsProp,
658
- onDisplayOptionsChange,
659
- panelGroupsBuilder,
660
- panelRenderListRow,
661
- panelRenderDetail,
662
- },
663
- ref,
664
- ) {
665
- const router = useRouter()
666
- const displayOptions = React.useMemo(
667
- () => ({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...displayOptionsProp }),
668
- [displayOptionsProp],
669
- )
670
-
671
- const columns = React.useMemo(() => getPlacementColumns(), [])
672
- const tableData = ALL_PLACEMENTS
673
-
674
- const renderFilterOptionValue = React.useCallback(
675
- (fieldKey: string, value: string): React.ReactNode => {
676
- if (fieldKey === "status") return <StatusBadge status={value as Status} />
677
- const col = columns.find(c => c.key === fieldKey)
678
- const opt = col?.filter?.options?.find(o => o.value === value)
679
- return <span className="text-foreground">{opt?.label ?? value}</span>
680
- },
681
- [columns],
682
- )
683
-
684
- // ─ Pagination chrome (only TABLE + LIST views) ────────────────────────────
685
- const [pagination, setPagination] = React.useState(false)
686
- const [paginationPage, setPaginationPage] = React.useState(1)
687
- const [paginationPageSize, setPaginationPageSize] = React.useState(10)
688
- const [filteredCount, setFilteredCount] = React.useState(tableData.length)
689
-
690
- React.useEffect(() => {
691
- setFilteredCount(tableData.length)
692
- }, [tableData])
693
-
694
- const totalPages = Math.max(1, Math.ceil(filteredCount / Math.max(1, paginationPageSize)))
695
- const safePage = Math.min(paginationPage, totalPages)
696
-
697
- // Pagination only applies to TABLE + LIST views (board/dashboard/folder/panel/tree-panel
698
- // are not paged). Cards/boards consume `state.rows` directly.
699
- const paginationEligible = view === "table" || view === "list"
700
- const paginationOverride =
701
- pagination && paginationEligible ? { page: safePage, pageSize: paginationPageSize } : undefined
702
-
703
- const onPageSizeChange = React.useCallback((n: number) => {
704
- setPaginationPageSize(n)
705
- setPaginationPage(1)
706
- }, [])
707
-
708
- // Renderers --------------------------------------------------------------
709
- const renderers: HubTableRenderers<Placement> = {
710
- "board-with-toolbar": (args) =>
711
- args.toolbarShell(<PlacementsBoardBody args={args} />),
712
- "list-with-toolbar": (args) => {
713
- const { state, toolbarShell } = args
714
- const listRows = pagination ? (state.pagedRows as Placement[]) : (state.rows as Placement[])
715
- const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
716
- return (
717
- <>
718
- {pagination ? (
719
- <CountSyncer
720
- count={state.rows.length}
721
- onSync={setFilteredCount}
722
- onReset={() => setPaginationPage(1)}
723
- />
724
- ) : null}
725
- {toolbarShell(
726
- <>
727
- <DataRowList<Placement>
728
- rows={listRows}
729
- getRowId={row => row.id}
730
- ariaLabel="Placements"
731
- emptyState={PLACEMENT_EMPTY_COPY}
732
- virtualizeThreshold={PLACEMENT_LIST_VIRTUAL_ROWS_THRESHOLD}
733
- estimatedRowHeight={PLACEMENT_LIST_ESTIMATE_ROW_PX}
734
- renderRow={row => (
735
- <PlacementListRowContent
736
- row={row}
737
- hiddenColKeys={state.hiddenCols}
738
- boardColumns={boardColumns}
739
- conditionalRules={args.drawerToolbarProps.conditionalRules}
740
- onOpen={id => router.push(`/data-list/${id}`)}
741
- />
742
- )}
743
- />
744
- {pagination ? (
745
- <div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
746
- <PaginationBar
747
- page={safePage}
748
- pageSize={paginationPageSize}
749
- total={filteredCount}
750
- pageSizeOptions={[10, 25, 50, 100]}
751
- onPageChange={setPaginationPage}
752
- onPageSizeChange={onPageSizeChange}
753
- />
754
- </div>
755
- ) : null}
756
- </>,
757
- )}
758
- </>
759
- )
760
- },
761
- "folder-with-toolbar": (args) => {
762
- const { state, toolbarShell } = args
763
- const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
764
- return toolbarShell(
765
- <FolderGridView<Placement>
766
- rows={state.rows as Placement[]}
767
- getRowId={r => r.id}
768
- ariaLabel="Demo folder view"
769
- emptyContent={<p>{PLACEMENT_EMPTY_COPY}</p>}
770
- renderTile={row => (
771
- <PlacementFolderTile
772
- row={row}
773
- hiddenColKeys={state.hiddenCols}
774
- boardColumns={boardColumns}
775
- onClick={() => router.push(`/data-list/${row.id}`)}
776
- />
777
- )}
778
- />,
779
- )
780
- },
781
- "tree-panel-with-toolbar": (args) =>
782
- args.toolbarShell(<PlacementsTreeBody args={args} />),
783
- "panel-with-toolbar": (args) => {
784
- const { state, toolbarShell } = args
785
- const listRows = state.rows as Placement[]
786
- const boardColumns = state.displayCols.filter(c => c.key !== "select" && c.key !== "actions")
787
- const groups = panelGroupsBuilder ? panelGroupsBuilder(listRows) : buildStatusGroups(listRows)
788
- return toolbarShell(
789
- <ListPageSplitHubChrome aria-label={PLACEMENT_DRAWER_LABEL}>
790
- <FinderPanelView<Placement>
791
- embedded
792
- groupsColumnTitle="Status"
793
- groups={groups}
794
- rows={listRows}
795
- getRowId={r => r.id}
796
- getRowGroupId={r => r.status}
797
- defaultGroupId="all"
798
- autoSaveId="finder-panel-view"
799
- ariaLabel="Demo panel view"
800
- emptyList={<p>{PLACEMENT_EMPTY_COPY}</p>}
801
- renderListRow={
802
- panelRenderListRow
803
- ? panelRenderListRow
804
- : (row, isSelected) => (
805
- <PlacementFinderListRow
806
- row={row}
807
- isSelected={isSelected}
808
- hiddenColKeys={state.hiddenCols}
809
- boardColumns={boardColumns}
810
- />
811
- )
812
- }
813
- renderDetail={
814
- panelRenderDetail
815
- ? panelRenderDetail
816
- : row => (
817
- <PlacementFinderDetail
818
- row={row}
819
- hiddenColKeys={state.hiddenCols}
820
- boardColumns={boardColumns}
821
- />
822
- )
823
- }
824
- />
825
- </ListPageSplitHubChrome>,
826
- )
827
- },
828
- "dashboard-with-toolbar": (args) => <PlacementsDashboardBody args={args} columns={columns} />,
829
- }
830
-
831
- // Custom `tableRenderer` so pagination chrome (CountSyncer + PaginationBar) wraps the
832
- // default DataTable when `pagination` is on. When off, falls back to a plain DataTable.
833
- const tableRenderer = (args: HubTableRendererArgs<Placement>) => {
834
- const { state } = args
835
- return (
836
- <>
837
- {pagination ? (
838
- <CountSyncer
839
- count={state.rows.length}
840
- onSync={setFilteredCount}
841
- onReset={() => setPaginationPage(1)}
842
- />
843
- ) : null}
844
- <div className="pb-6">
845
- <DataTable<Placement>
846
- data={tableData}
847
- columns={columns}
848
- getRowId={row => row.id}
849
- getRowSelectionLabel={row => row.student}
850
- selectable
851
- searchable={displayOptions.showToolbarSearch}
852
- showColumnHeaders={displayOptions.showColumnLabels}
853
- defaultSort={{ key: "student" as const, dir: "asc" as const }}
854
- emptyState={PLACEMENT_EMPTY_COPY}
855
- renderFilterOptionValue={renderFilterOptionValue}
856
- conditionalRules={args.drawerToolbarProps.conditionalRules}
857
- onRowClick={row => router.push(`/data-list/${row.id}`)}
858
- state={state}
859
- hasFooter={pagination}
860
- toolbarSlot={s => (
861
- <TablePropertiesDrawerButton {...args.drawerToolbarProps} state={s} />
862
- )}
863
- bulkActionsSlot={(selected) => {
864
- const count = selected.size
865
- if (count === 0) return null
866
- const contextId = "bulk-selection-context"
867
- return (
868
- <>
869
- <span id={contextId} className="sr-only">
870
- {count} {count === 1 ? "row" : "rows"} selected
871
- </span>
872
- <Button size="sm" variant="default" aria-describedby={contextId}>
873
- <i className="fa-light fa-circle-check" aria-hidden="true" /> Confirm
874
- </Button>
875
- <Button size="sm" variant="outline" aria-describedby={contextId}>
876
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" /> Export
877
- </Button>
878
- <Button size="sm" variant="destructive" aria-describedby={contextId}>
879
- <i className="fa-light fa-trash" aria-hidden="true" /> Delete
880
- </Button>
881
- </>
882
- )
883
- }}
884
- />
885
- </div>
886
- {pagination ? (
887
- <div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
888
- <PaginationBar
889
- page={safePage}
890
- pageSize={paginationPageSize}
891
- total={filteredCount}
892
- pageSizeOptions={[10, 25, 50, 100]}
893
- onPageChange={setPaginationPage}
894
- onPageSizeChange={onPageSizeChange}
895
- />
896
- </div>
897
- ) : null}
898
- </>
899
- )
900
- }
901
-
902
- return (
903
- <HubTable<Placement>
904
- rows={tableData}
905
- columns={columns}
906
- view={view}
907
- onViewChange={onViewChange}
908
- supportedViewTypes={PLACEMENTS_SUPPORTED_VIEWS}
909
- hubLabel={PLACEMENT_DRAWER_LABEL}
910
- lifecycleTabLabel={PLACEMENT_DRAWER_LABEL}
911
- searchAriaLabel="Search rows"
912
- getRowId={row => row.id}
913
- getRowSelectionLabel={row => row.student}
914
- defaultSort={{ key: "student", dir: "asc" }}
915
- emptyState={PLACEMENT_EMPTY_COPY}
916
- onRowClick={row => router.push(`/data-list/${row.id}`)}
917
- displayOptions={displayOptions}
918
- onDisplayOptionsChange={onDisplayOptionsChange}
919
- pagination={pagination}
920
- onPaginationChange={setPagination}
921
- paginationOverride={paginationOverride}
922
- renderFilterOptionValue={renderFilterOptionValue}
923
- renderers={renderers}
924
- tableRenderer={tableRenderer}
925
- handleRef={ref}
926
- />
927
- )
928
- })
929
-
930
- PlacementsTable.displayName = "PlacementsTable"
931
-
932
- export type { DataListViewType } from "@/lib/data-list-view"
933
- export { DATA_LIST_VIEW_TILES } from "@/lib/data-list-view"
934
- export type { DataListDisplayOptions } from "@/lib/data-list-display-options"