@exxatdesignux/ui 0.0.6 → 0.0.8

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,239 @@
1
+ "use client"
2
+
3
+ /**
4
+ * DataListClient — Placements page built on the reusable ListPageTemplate.
5
+ *
6
+ * Uses centralized exports from `@/components/data-views` (same pattern as Team / Compliance).
7
+ */
8
+
9
+ import * as React from "react"
10
+ import { useRouter } from "next/navigation"
11
+ import { useSidebar } from "@/components/ui/sidebar"
12
+ import {
13
+ ListPageTemplate,
14
+ type ViewTab,
15
+ DataListTable,
16
+ type DataListTableHandle,
17
+ type PlacementLifecycleTabId,
18
+ type DataListViewType,
19
+ dataListViewIcon,
20
+ } from "@/components/data-views"
21
+ import {
22
+ emptyCopyForPlacementLifecycleTab,
23
+ getPlacementColumnsForLifecycle,
24
+ placementLifecycleDrawerLabels,
25
+ } from "@/components/placements-table-columns"
26
+ import { PlacementsPageHeader } from "@/components/placements-page-header"
27
+ import {
28
+ DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
29
+ type DataListDisplayOptions,
30
+ } from "@/lib/data-list-display-options"
31
+ import { loadPageFromStorage, schedulePageSave } from "@/lib/data-list-persistence"
32
+ import { KeyMetrics } from "@/components/key-metrics"
33
+ import { placementsForPhase } from "@/lib/mock/placements"
34
+ import { PLACEMENT_KPI_INSIGHT, PLACEMENT_KPI_METRICS } from "@/lib/mock/placements-kpi"
35
+ import { useAskLeoPageContext } from "@/components/ask-leo-sidebar"
36
+ import { CoachMark } from "@/components/ui/coach-mark"
37
+ import { useCoachMark, type CoachMarkStep } from "@/hooks/use-coach-mark"
38
+
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+ // Coach mark flow — Views & Properties tour
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+
43
+ const VIEWS_TOUR_STEPS: CoachMarkStep[] = [
44
+ {
45
+ id: "views-tabs",
46
+ target: "[role='toolbar'][aria-label='Views']",
47
+ side: "bottom",
48
+ align: "start",
49
+ title: "Switch Between Views",
50
+ description:
51
+ "Use these tabs to filter your placements by status — All, Upcoming, Ongoing, or Completed. Each view remembers its own settings.",
52
+ },
53
+ {
54
+ id: "views-settings",
55
+ target: "[aria-label='View settings']",
56
+ side: "bottom",
57
+ align: "start",
58
+ title: "Customise Each View",
59
+ description:
60
+ "Click the dropdown arrow to rename, duplicate, or edit a view. Choose between Table, List, Board, or Dashboard layouts.",
61
+ },
62
+ {
63
+ id: "views-add",
64
+ target: "button:has(.fa-plus) + .fa-plus, [aria-label='Views'] ~ button",
65
+ side: "bottom",
66
+ align: "start",
67
+ title: "Create New Views",
68
+ description:
69
+ "Add custom views with different layouts and filters. Create a Board view for visual tracking, or a Dashboard for charts and KPIs.",
70
+ },
71
+ {
72
+ id: "views-search",
73
+ target: "button[aria-label='Search']",
74
+ side: "bottom",
75
+ align: "end",
76
+ title: "Quick Search",
77
+ description:
78
+ "Instantly search across all visible columns. Press ⌘K to open search from anywhere on the page.",
79
+ },
80
+ {
81
+ id: "views-filter",
82
+ target: "button[aria-label='Add filter']:last-of-type, button:has(.fa-filter-list)",
83
+ side: "bottom",
84
+ align: "end",
85
+ title: "Filter Your Data",
86
+ description:
87
+ "Add filters to narrow down results. Combine multiple conditions — filter by status, date, site, program, and more.",
88
+ },
89
+ {
90
+ id: "views-properties",
91
+ target: "button[aria-label='Properties']",
92
+ side: "bottom",
93
+ align: "end",
94
+ title: "Table Properties",
95
+ description:
96
+ "Open the Properties panel to manage columns, sort order, conditional formatting, density, and gridlines. Everything is saved per view.",
97
+ },
98
+ ]
99
+
100
+ // ─────────────────────────────────────────────────────────────────────────────
101
+ // Config
102
+ // ─────────────────────────────────────────────────────────────────────────────
103
+
104
+ const DEFAULT_TABS: ViewTab[] = [
105
+ { id: "all", label: "All", viewType: "table", icon: "fa-table", filterId: "all" },
106
+ { id: "upcoming", label: "Upcoming", viewType: "table", icon: "fa-calendar-clock", filterId: "upcoming" },
107
+ { id: "ongoing", label: "Ongoing", viewType: "table", icon: "fa-circle-half-stroke", filterId: "ongoing" },
108
+ { id: "completed", label: "Completed", viewType: "table", icon: "fa-circle-check", filterId: "completed" },
109
+ ]
110
+
111
+ const LIFECYCLE_OPTIONS = [
112
+ { id: "all", label: "All" },
113
+ { id: "upcoming", label: "Upcoming" },
114
+ { id: "ongoing", label: "Ongoing" },
115
+ { id: "completed", label: "Completed" },
116
+ ]
117
+
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+ // Component
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+
122
+ export function DataListClient() {
123
+ const router = useRouter()
124
+ const { setOpen } = useSidebar()
125
+ const [showMetrics, setShowMetrics] = React.useState(true)
126
+ const [exportOpen, setExportOpen] = React.useState(false)
127
+ const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
128
+ const [tabs, setTabs] = React.useState<ViewTab[]>(DEFAULT_TABS)
129
+ const [activeTabId, setActiveTabId] = React.useState<string>(DEFAULT_TABS[0]?.id ?? "")
130
+ const tableRef = React.useRef<DataListTableHandle>(null)
131
+
132
+ const viewsTour = useCoachMark({
133
+ flowId: "placements-views-tour",
134
+ steps: VIEWS_TOUR_STEPS,
135
+ delay: 1200,
136
+ })
137
+
138
+ const activeTab = tabs.find((t) => t.id === activeTabId)
139
+ const placementCount = activeTab
140
+ ? placementsForPhase(activeTab.filterId as PlacementLifecycleTabId).length
141
+ : 0
142
+
143
+ useAskLeoPageContext(
144
+ React.useMemo(
145
+ () => ({
146
+ title: "Placements",
147
+ description: activeTab
148
+ ? `${placementCount} row${placementCount === 1 ? "" : "s"} in “${activeTab.label}” · ${activeTab.viewType} view.`
149
+ : undefined,
150
+ suggestions: [
151
+ "Which placements end in the next 30 days?",
152
+ "Summarize what’s in this view after my filters",
153
+ "What columns should I add for site coordinators?",
154
+ ],
155
+ }),
156
+ [activeTab, placementCount],
157
+ ),
158
+ )
159
+
160
+ React.useLayoutEffect(() => {
161
+ const p = loadPageFromStorage()
162
+ if (!p) return
163
+ setDisplayOptions(prev => ({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...p.displayOptions }))
164
+ setShowMetrics(p.showMetrics)
165
+ setTabs(p.tabs)
166
+ const nextActive = p.tabs.some(t => t.id === p.activeTabId) ? p.activeTabId : (p.tabs[0]?.id ?? "")
167
+ setActiveTabId(nextActive)
168
+ }, [])
169
+
170
+ React.useEffect(() => {
171
+ schedulePageSave({
172
+ v: 1,
173
+ displayOptions,
174
+ showMetrics,
175
+ tabs,
176
+ activeTabId,
177
+ })
178
+ }, [displayOptions, showMetrics, tabs, activeTabId])
179
+
180
+ function handleNewPlacement() {
181
+ setOpen(false)
182
+ setTimeout(() => router.push("/data-list/new"), 260)
183
+ }
184
+
185
+ return (
186
+ <>
187
+ <CoachMark state={viewsTour} />
188
+ <ListPageTemplate
189
+ tabs={tabs}
190
+ onTabsChange={setTabs}
191
+ activeTabId={activeTabId}
192
+ onActiveTabChange={setActiveTabId}
193
+ tablePropertiesRef={tableRef}
194
+ header={
195
+ <PlacementsPageHeader
196
+ onNewPlacement={handleNewPlacement}
197
+ onExport={() => setExportOpen(true)}
198
+ showMetrics={showMetrics}
199
+ onToggleMetrics={() => setShowMetrics(v => !v)}
200
+ showTitleBlock={displayOptions.showViewTitle}
201
+ />
202
+ }
203
+ metrics={
204
+ <KeyMetrics
205
+ variant="flat"
206
+ metrics={PLACEMENT_KPI_METRICS}
207
+ insight={PLACEMENT_KPI_INSIGHT}
208
+ showHeader={false}
209
+ metricsSingleRow
210
+ />
211
+ }
212
+ showMetrics={showMetrics}
213
+ defaultTabs={DEFAULT_TABS}
214
+ filterOptions={LIFECYCLE_OPTIONS}
215
+ filterLabel="Filter lifecycle"
216
+ getTabCount={(filterId) => placementsForPhase(filterId as PlacementLifecycleTabId).length}
217
+ renderContent={(tab, updateTab) => (
218
+ <DataListTable
219
+ key={tab.id}
220
+ ref={tableRef}
221
+ view={tab.viewType}
222
+ onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
223
+ lifecycleTabId={tab.filterId as PlacementLifecycleTabId}
224
+ getColumnsForLifecycle={getPlacementColumnsForLifecycle}
225
+ emptyTableCopy={emptyCopyForPlacementLifecycleTab(tab.filterId as PlacementLifecycleTabId)}
226
+ lifecycleDrawerLabel={
227
+ placementLifecycleDrawerLabels[tab.filterId as PlacementLifecycleTabId]
228
+ }
229
+ displayOptions={displayOptions}
230
+ onDisplayOptionsChange={patch =>
231
+ setDisplayOptions(prev => ({ ...prev, ...patch }))}
232
+ />
233
+ )}
234
+ exportOpen={exportOpen}
235
+ onExportOpenChange={setExportOpen}
236
+ />
237
+ </>
238
+ )
239
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { render, screen } from "@testing-library/react"
3
+
4
+ import { HireBadge, ReadinessBadge, StatusBadge } from "./data-list-table-cells"
5
+
6
+ describe("data-list-table-cells", () => {
7
+ it("renders StatusBadge label for confirmed", () => {
8
+ render(<StatusBadge status="confirmed" />)
9
+ expect(screen.getByText("Confirmed")).toBeInTheDocument()
10
+ })
11
+
12
+ it("ReadinessBadge uses destructive variant for risk copy", () => {
13
+ const { container } = render(<ReadinessBadge value="At risk" />)
14
+ expect(container.querySelector("[data-slot='badge']")).toBeTruthy()
15
+ expect(screen.getByText("At risk")).toBeInTheDocument()
16
+ })
17
+
18
+ it("HireBadge shows em dash for empty", () => {
19
+ render(<HireBadge value="" />)
20
+ expect(screen.getByText("—")).toBeInTheDocument()
21
+ })
22
+ })
@@ -0,0 +1,173 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Placement table cell primitives — extracted from data-list-table for reuse and easier testing.
5
+ */
6
+
7
+ import * as React from "react"
8
+ import { Badge } from "@/components/ui/badge"
9
+ import { Button } from "@/components/ui/button"
10
+ import { Tip } from "@/components/ui/tip"
11
+ import {
12
+ DropdownMenu,
13
+ DropdownMenuContent,
14
+ DropdownMenuItem,
15
+ DropdownMenuSeparator,
16
+ DropdownMenuTrigger,
17
+ } from "@/components/ui/dropdown-menu"
18
+ import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
19
+ import {
20
+ PLACEMENT_STATUS_BADGE_CLASS,
21
+ PLACEMENT_STATUS_ICON,
22
+ PLACEMENT_STATUS_LABEL,
23
+ } from "@/lib/list-status-badges"
24
+ import { AvatarInitials } from "@/components/ui/avatar"
25
+ import type { Placement, Status } from "@/lib/mock/placements"
26
+
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+ // Placement status — same maps + shell as other list hubs (`list-status-badges`)
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+
31
+ function isPlacementStatus(v: string): v is Status {
32
+ return v in PLACEMENT_STATUS_LABEL
33
+ }
34
+
35
+ export function StatusBadge({
36
+ status,
37
+ surface = "table",
38
+ }: {
39
+ status: Status | string
40
+ surface?: "table" | "board"
41
+ }) {
42
+ if (!isPlacementStatus(status)) {
43
+ return (
44
+ <Badge variant="outline" className="text-xs shrink-0">
45
+ {String(status)}
46
+ </Badge>
47
+ )
48
+ }
49
+ return (
50
+ <ListHubStatusBadge
51
+ surface={surface}
52
+ label={PLACEMENT_STATUS_LABEL[status]}
53
+ tintClassName={PLACEMENT_STATUS_BADGE_CLASS[status]}
54
+ icon={PLACEMENT_STATUS_ICON[status]}
55
+ />
56
+ )
57
+ }
58
+
59
+ export function AvatarCircle({ initials }: { initials: string }) {
60
+ return (
61
+ <AvatarInitials
62
+ initials={initials}
63
+ className="size-7 shrink-0 text-xs"
64
+ fallbackClassName="text-xs"
65
+ />
66
+ )
67
+ }
68
+
69
+ export function WeeksProgressCell({ row }: { row: Placement }) {
70
+ const { progressWeeksDone, progressWeeksTotal } = row
71
+ const total = Math.max(1, progressWeeksTotal)
72
+ const pct = Math.min(100, Math.round((progressWeeksDone / total) * 100))
73
+ return (
74
+ <div className="flex min-w-[128px] max-w-[200px] flex-col gap-1.5">
75
+ <div className="h-2 overflow-hidden rounded-full bg-muted">
76
+ <div
77
+ className="h-full rounded-full bg-primary transition-[width]"
78
+ style={{ width: `${pct}%` }}
79
+ />
80
+ </div>
81
+ <span className="text-xs tabular-nums text-muted-foreground">
82
+ {progressWeeksDone} / {progressWeeksTotal} wks
83
+ </span>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ export function ReadinessBadge({ value }: { value: string }) {
89
+ const lower = value.toLowerCase()
90
+ const variant =
91
+ lower.includes("risk") || lower.includes("blocked")
92
+ ? "destructive"
93
+ : lower.includes("review")
94
+ ? "secondary"
95
+ : "outline"
96
+ return (
97
+ <Badge variant={variant} className="h-6 px-2 py-1 text-xs font-medium leading-none">
98
+ {value}
99
+ </Badge>
100
+ )
101
+ }
102
+
103
+ export function HireBadge({ value }: { value: string }) {
104
+ if (value === "—" || !value) return <span className="text-sm text-muted-foreground">—</span>
105
+ const yes = value.toLowerCase() === "yes"
106
+ return (
107
+ <Badge
108
+ variant={yes ? "default" : "secondary"}
109
+ className="h-6 border-0 px-2 py-1 text-xs font-medium leading-none"
110
+ >
111
+ {value}
112
+ </Badge>
113
+ )
114
+ }
115
+
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+ // Row actions
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+
120
+ export interface RowActionDef {
121
+ label: string
122
+ icon: string
123
+ onClick: (row: Placement) => void
124
+ variant?: "destructive"
125
+ }
126
+
127
+ export const PLACEMENT_ROW_ACTIONS: RowActionDef[] = [
128
+ { label: "Edit", icon: "fa-pen-to-square", onClick: _row => {} },
129
+ { label: "Open", icon: "fa-arrow-up-right", onClick: _row => {} },
130
+ { label: "Delete", icon: "fa-trash", onClick: _row => {}, variant: "destructive" },
131
+ ]
132
+
133
+ export function RowActions({ row, actions }: { row: Placement; actions: RowActionDef[] }) {
134
+ if (!actions.length) return null
135
+
136
+ if (actions.length === 1) {
137
+ const a = actions[0]
138
+ return (
139
+ <Tip label={a.label} side="top">
140
+ <Button size="icon-sm" variant="ghost" aria-label={`${a.label} ${row.student}`}
141
+ onClick={() => a.onClick(row)}>
142
+ <i className={`fa-light ${a.icon} text-sm`} aria-hidden="true" />
143
+ </Button>
144
+ </Tip>
145
+ )
146
+ }
147
+
148
+ return (
149
+ <DropdownMenu>
150
+ <Tip label={`More options for ${row.student}`} side="top">
151
+ <DropdownMenuTrigger asChild>
152
+ <Button size="icon-sm" variant="ghost" aria-label={`More options for ${row.student}`}>
153
+ <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
154
+ </Button>
155
+ </DropdownMenuTrigger>
156
+ </Tip>
157
+ <DropdownMenuContent align="end" className="w-40">
158
+ {actions.map((a, i) => (
159
+ <React.Fragment key={a.label}>
160
+ {a.variant === "destructive" && i > 0 && <DropdownMenuSeparator />}
161
+ <DropdownMenuItem
162
+ onClick={() => a.onClick(row)}
163
+ className={a.variant === "destructive" ? "text-destructive focus:text-destructive" : ""}
164
+ >
165
+ <i className={`fa-light ${a.icon}`} aria-hidden="true" />
166
+ {a.label}
167
+ </DropdownMenuItem>
168
+ </React.Fragment>
169
+ ))}
170
+ </DropdownMenuContent>
171
+ </DropdownMenu>
172
+ )
173
+ }