@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,250 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Placement-specific board card — composes shared board primitives with
5
- * column defs and shared layout helpers (no lifecycle parameterisation).
6
- */
7
-
8
- import * as React from "react"
9
- import { cn } from "@/lib/utils"
10
- import type { Placement } from "@/lib/mock/placements"
11
- import { StatusBadge } from "@/components/placements-table-cells"
12
- import { AvatarInitials } from "@/components/ui/avatar"
13
- import { Badge } from "@/components/ui/badge"
14
- import {
15
- ListPageBoardCard,
16
- ListPageBoardCardBadgeRow,
17
- ListPageBoardCardBody,
18
- ListPageBoardCardHeader,
19
- ListPageBoardCardSecondary,
20
- ListPageBoardCardTitleRow,
21
- } from "@/components/data-views/list-page-board-card"
22
- import type { BoardLineCount } from "@/lib/data-list-display-options"
23
- import {
24
- filterColumnsForBoardCard,
25
- isBoardFieldActive,
26
- remainingBodyColumns,
27
- } from "@/lib/placement-board-card-layout"
28
- import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
29
- import type { ConditionalRule } from "@/components/table-properties/types"
30
- import type { CellContext, ColumnDef } from "@/components/data-table/types"
31
- import {
32
- BoardCardIconRow,
33
- BoardCardTwoLineBlock,
34
- lineClampClass,
35
- } from "@/components/data-views/board-card-primitives"
36
-
37
- const BOARD_CELL_CTX: CellContext<Placement> = {
38
- rowIndex: 0,
39
- selected: false,
40
- onSelect: () => {},
41
- }
42
-
43
- function columnIconClass(col: ColumnDef<Placement>): string {
44
- if (col.filter?.icon) return col.filter.icon
45
- const fallbacks: Record<string, string> = {
46
- specialization: "fa-stethoscope",
47
- site: "fa-hospital",
48
- internship: "fa-briefcase",
49
- supervisor: "fa-user-tie",
50
- start: "fa-calendar-days",
51
- compliance: "fa-shield-check",
52
- daysUntilStart: "fa-calendar-days",
53
- readiness: "fa-flag",
54
- progressWeeksDone: "fa-chart-line",
55
- endDate: "fa-calendar-xmark",
56
- lastCheckin: "fa-calendar-clock",
57
- completionDate: "fa-calendar-check",
58
- finalStatus: "fa-circle-check",
59
- rating: "fa-star",
60
- suggestedToHire: "fa-user-check",
61
- duration: "fa-clock",
62
- program: "fa-graduation-cap",
63
- student: "fa-user",
64
- status: "fa-circle-dot",
65
- }
66
- return fallbacks[col.key] ?? "fa-tag"
67
- }
68
-
69
- function boardCellContent(row: Placement, col: ColumnDef<Placement>): React.ReactNode {
70
- if (col.key === "status") return <StatusBadge status={row.status} surface="board" />
71
- if (col.cell) return col.cell(row, BOARD_CELL_CTX)
72
- return <span className="text-foreground/90">{String(row[col.key as keyof Placement] ?? "")}</span>
73
- }
74
-
75
- function renderScheduleSection(
76
- row: Placement,
77
- hiddenColKeys: Set<string>,
78
- boardColumns: ColumnDef<Placement>[],
79
- ): React.ReactNode {
80
- const phase = row.placementPhase
81
- const aStart = isBoardFieldActive("start", hiddenColKeys, boardColumns)
82
- const aDur = isBoardFieldActive("duration", hiddenColKeys, boardColumns)
83
- const aDays = isBoardFieldActive("daysUntilStart", hiddenColKeys, boardColumns)
84
- const aProg = isBoardFieldActive("progressWeeksDone", hiddenColKeys, boardColumns)
85
- const aEnd = isBoardFieldActive("endDate", hiddenColKeys, boardColumns)
86
- const aComp = isBoardFieldActive("completionDate", hiddenColKeys, boardColumns)
87
- const aFinal = isBoardFieldActive("finalStatus", hiddenColKeys, boardColumns)
88
-
89
- if (phase === "upcoming" && (aStart || aDays)) {
90
- const line2 =
91
- aDays && row.daysUntilStart > 0
92
- ? `Starts in ${row.daysUntilStart} days`
93
- : aDays && row.daysUntilStart === 0
94
- ? "Starts today"
95
- : aDur
96
- ? row.duration
97
- : "—"
98
- return (
99
- <BoardCardTwoLineBlock
100
- iconClass="fa-calendar-days"
101
- line1={aStart ? row.start : "—"}
102
- line2={line2}
103
- />
104
- )
105
- }
106
- if (phase === "ongoing" && (aProg || aEnd)) {
107
- return (
108
- <BoardCardTwoLineBlock
109
- iconClass="fa-calendar-days"
110
- line1={aProg ? `${row.progressWeeksDone} / ${row.progressWeeksTotal} wks` : "—"}
111
- line2={aEnd ? row.endDate : "—"}
112
- />
113
- )
114
- }
115
- if (phase === "completed" && (aComp || aFinal)) {
116
- const finalCol = boardColumns.find(c => c.key === "finalStatus")
117
- return (
118
- <BoardCardTwoLineBlock
119
- iconClass="fa-calendar-check"
120
- line1={aComp ? row.completionDate : "—"}
121
- line2={
122
- aFinal && finalCol ? (
123
- <span className="inline-flex min-w-0 max-w-full [&_span]:text-xs">
124
- {boardCellContent(row, finalCol)}
125
- </span>
126
- ) : aFinal ? (
127
- row.finalStatus
128
- ) : (
129
- "—"
130
- )
131
- }
132
- line2ClassName={aFinal && finalCol ? "text-xs" : undefined}
133
- />
134
- )
135
- }
136
- if (aStart || aDur) {
137
- return (
138
- <BoardCardTwoLineBlock
139
- iconClass="fa-calendar-days"
140
- line1={aStart ? row.start : "—"}
141
- line2={aDur ? row.duration : "—"}
142
- />
143
- )
144
- }
145
- return null
146
- }
147
-
148
- export function BoardPlacementCard({
149
- row,
150
- hiddenColKeys,
151
- lineCount,
152
- conditionalRules,
153
- boardColumns,
154
- onOpen,
155
- }: {
156
- row: Placement
157
- hiddenColKeys: Set<string>
158
- lineCount: BoardLineCount
159
- conditionalRules: ConditionalRule[] | undefined
160
- boardColumns: ColumnDef<Placement>[]
161
- onOpen: (id: number) => void
162
- }) {
163
- const lc = lineClampClass(lineCount)
164
- const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
165
-
166
- const visibleCols = boardColumns.filter(c => !hiddenColKeys.has(c.key))
167
- const showStudent = visibleCols.some(c => c.key === "student")
168
- const cardCols = filterColumnsForBoardCard(visibleCols)
169
- const remainingCols = remainingBodyColumns(cardCols)
170
-
171
- const showStatus = isBoardFieldActive("status", hiddenColKeys, boardColumns)
172
- const showSite = isBoardFieldActive("site", hiddenColKeys, boardColumns)
173
- const siteCol = boardColumns.find(c => c.key === "site")
174
-
175
- const cardShell = (className: string, children: React.ReactNode) => (
176
- <ListPageBoardCard
177
- className={className}
178
- style={ruleBg ? { background: ruleBg } : undefined}
179
- isNew={row.isNew}
180
- onClick={() => onOpen(row.id)}
181
- >
182
- {children}
183
- </ListPageBoardCard>
184
- )
185
-
186
- if (visibleCols.length === 0) {
187
- return cardShell(
188
- "cursor-pointer",
189
- <ListPageBoardCardHeader className="gap-1 pb-2">
190
- <ListPageBoardCardTitleRow title={`Placement #${row.id}`} />
191
- <ListPageBoardCardSecondary>
192
- Unhide columns in Properties → Columns to show card fields.
193
- </ListPageBoardCardSecondary>
194
- </ListPageBoardCardHeader>,
195
- )
196
- }
197
-
198
- const titlePrimary = showStudent ? row.student : `Placement ${row.id}`
199
- const headerBadgeRow = showStatus || row.isNew
200
-
201
- return cardShell(
202
- "cursor-pointer",
203
- <ListPageBoardCardHeader>
204
- <ListPageBoardCardTitleRow
205
- title={titlePrimary}
206
- titleClassName={lc}
207
- trailing={
208
- showStudent ? (
209
- <AvatarInitials
210
- initials={row.initials}
211
- className="size-7 shrink-0 text-xs"
212
- fallbackClassName="text-xs"
213
- />
214
- ) : undefined
215
- }
216
- />
217
-
218
- {headerBadgeRow ? (
219
- <ListPageBoardCardBadgeRow>
220
- {showStatus ? <StatusBadge status={row.status} surface="board" /> : null}
221
- {row.isNew ? (
222
- <Badge variant="secondary" className="h-6 px-2 text-xs font-medium">
223
- New
224
- </Badge>
225
- ) : null}
226
- </ListPageBoardCardBadgeRow>
227
- ) : null}
228
-
229
- <ListPageBoardCardBody>
230
- {showSite && siteCol ? (
231
- <BoardCardIconRow iconClass="fa-hospital">
232
- <div className={cn(lc, "[&_.text-sm]:text-xs")}>{boardCellContent(row, siteCol)}</div>
233
- </BoardCardIconRow>
234
- ) : null}
235
-
236
- {renderScheduleSection(row, hiddenColKeys, boardColumns)}
237
-
238
- {remainingCols.length > 0 ? (
239
- <div className="flex flex-col gap-2">
240
- {remainingCols.map(col => (
241
- <BoardCardIconRow key={col.key} iconClass={columnIconClass(col)}>
242
- <div className={cn(lc)}>{boardCellContent(row, col)}</div>
243
- </BoardCardIconRow>
244
- ))}
245
- </div>
246
- ) : null}
247
- </ListPageBoardCardBody>
248
- </ListPageBoardCardHeader>,
249
- )
250
- }
@@ -1,438 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { useState } from "react"
5
- import { Badge } from "@/components/ui/badge"
6
- import { Button } from "@/components/ui/button"
7
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
8
- import { Tip } from "@/components/ui/tip"
9
- import { Avatar, AvatarFallback } from "@/components/ui/avatar"
10
- import { Separator } from "@/components/ui/separator"
11
- import {
12
- DropdownMenu,
13
- DropdownMenuContent,
14
- DropdownMenuItem,
15
- DropdownMenuSeparator,
16
- DropdownMenuTrigger,
17
- } from "@/components/ui/dropdown-menu"
18
- import { cn } from "@/lib/utils"
19
- import type { Placement } from "@/lib/mock/placements"
20
- import { StatusBadge as PlacementStatusBadge } from "@/components/placements-table-cells"
21
- import { placementReadinessBadgeClass } from "@/lib/list-status-badges"
22
-
23
- // ─────────────────────────────────────────────────────────────────────────────
24
- // Info row — <dl> pattern for structured data
25
- // ─────────────────────────────────────────────────────────────────────────────
26
-
27
- function InfoRow({
28
- label,
29
- value,
30
- icon,
31
- }: {
32
- label: string
33
- value: React.ReactNode
34
- icon?: string
35
- }) {
36
- return (
37
- <div className="flex items-start gap-3 py-2.5">
38
- {icon && (
39
- <i
40
- className={cn(
41
- "fa-light",
42
- icon,
43
- "text-muted-foreground text-[13px] mt-0.5 w-4 shrink-0"
44
- )}
45
- aria-hidden="true"
46
- />
47
- )}
48
- <dt className="text-sm text-muted-foreground w-32 shrink-0">{label}</dt>
49
- <dd className="text-sm text-foreground">{value}</dd>
50
- </div>
51
- )
52
- }
53
-
54
- // ─────────────────────────────────────────────────────────────────────────────
55
- // Tabs config
56
- // ─────────────────────────────────────────────────────────────────────────────
57
-
58
- type TabId = "overview" | "schedule" | "compliance" | "activity"
59
-
60
- const TABS: { id: TabId; label: string; icon: string }[] = [
61
- { id: "overview", label: "Overview", icon: "fa-circle-info" },
62
- { id: "schedule", label: "Schedule", icon: "fa-calendar-days" },
63
- { id: "compliance", label: "Compliance", icon: "fa-shield-check" },
64
- { id: "activity", label: "Activity", icon: "fa-clock-rotate-left" },
65
- ]
66
-
67
- // ─────────────────────────────────────────────────────────────────────────────
68
- // Overview tab
69
- // ─────────────────────────────────────────────────────────────────────────────
70
-
71
- function OverviewTab({ placement }: { placement: Placement }) {
72
- return (
73
- <div className="grid gap-6 md:grid-cols-2" role="tabpanel" aria-label="Overview">
74
- <Card>
75
- <CardHeader>
76
- <CardTitle className="text-base">Placement Info</CardTitle>
77
- </CardHeader>
78
- <CardContent>
79
- <dl>
80
- <InfoRow icon="fa-hospital" label="Site" value={placement.site} />
81
- <InfoRow icon="fa-location-dot" label="Address" value={placement.siteAddress} />
82
- <Separator />
83
- <InfoRow icon="fa-briefcase" label="Internship" value={placement.internship} />
84
- <InfoRow icon="fa-stethoscope" label="Specialization" value={placement.specialization} />
85
- <InfoRow icon="fa-user-doctor" label="Supervisor" value={placement.supervisor} />
86
- <InfoRow icon="fa-user-nurse" label="Preceptor" value={placement.supervisor} />
87
- <Separator />
88
- <InfoRow icon="fa-rotate" label="Rotation type" value="Clinical" />
89
- <InfoRow icon="fa-graduation-cap" label="Credit hours" value="3" />
90
- </dl>
91
- </CardContent>
92
- </Card>
93
-
94
- <Card>
95
- <CardHeader>
96
- <CardTitle className="text-base">Supervisor &amp; Notes</CardTitle>
97
- </CardHeader>
98
- <CardContent>
99
- <dl>
100
- <InfoRow
101
- icon="fa-user"
102
- label="Supervisor"
103
- value={placement.supervisor}
104
- />
105
- <InfoRow
106
- icon="fa-envelope"
107
- label="Email"
108
- value={`${placement.supervisor.toLowerCase().replace(/\s|dr\.\s?/gi, ".")}@clinic.org`}
109
- />
110
- <InfoRow icon="fa-phone" label="Phone" value="(312) 555-0147" />
111
- <Separator />
112
- <InfoRow
113
- icon="fa-bullseye"
114
- label="Learning objectives"
115
- value="Develop clinical assessment skills and patient communication techniques in a supervised healthcare environment."
116
- />
117
- <InfoRow
118
- icon="fa-triangle-exclamation"
119
- label="Special requirements"
120
- value="Must complete CPR certification before start date. Scrubs required on-site."
121
- />
122
- <InfoRow
123
- icon="fa-note-sticky"
124
- label="Notes"
125
- value="Student has expressed interest in extending the placement if performance is satisfactory."
126
- />
127
- </dl>
128
- </CardContent>
129
- </Card>
130
- </div>
131
- )
132
- }
133
-
134
- // ─────────────────────────────────────────────────────────────────────────────
135
- // Schedule tab
136
- // ─────────────────────────────────────────────────────────────────────────────
137
-
138
- function ScheduleTab({ placement }: { placement: Placement }) {
139
- const progressPct =
140
- placement.progressWeeksTotal > 0
141
- ? Math.round(
142
- (placement.progressWeeksDone / placement.progressWeeksTotal) * 100
143
- )
144
- : 0
145
-
146
- return (
147
- <div role="tabpanel" aria-label="Schedule">
148
- <Card>
149
- <CardHeader>
150
- <CardTitle className="text-base">Schedule</CardTitle>
151
- </CardHeader>
152
- <CardContent>
153
- <dl className="grid gap-x-8 md:grid-cols-2">
154
- <InfoRow icon="fa-calendar-days" label="Start date" value={placement.start} />
155
- <InfoRow icon="fa-calendar-check" label="End date" value={placement.endDate} />
156
- <InfoRow icon="fa-clock" label="Duration" value={placement.duration} />
157
- <InfoRow icon="fa-hourglass-half" label="Hours/week" value="20" />
158
- <InfoRow icon="fa-sun" label="Shift" value="Day" />
159
- <InfoRow icon="fa-sigma" label="Total hours" value="240" />
160
- <InfoRow icon="fa-building" label="Work arrangement" value="On-site" />
161
- <InfoRow icon="fa-calendar-week" label="Weekends" value="No" />
162
- </dl>
163
-
164
- {placement.placementPhase === "ongoing" && (
165
- <>
166
- <Separator className="my-4" />
167
- <div className="space-y-2">
168
- <div className="flex items-center justify-between text-sm">
169
- <span className="text-muted-foreground">Progress</span>
170
- <span className="font-medium">
171
- {placement.progressWeeksDone} / {placement.progressWeeksTotal} weeks ({progressPct}%)
172
- </span>
173
- </div>
174
- <div
175
- className="h-2 w-full rounded-full bg-muted overflow-hidden"
176
- role="progressbar"
177
- aria-valuenow={progressPct}
178
- aria-valuemin={0}
179
- aria-valuemax={100}
180
- aria-label={`Placement progress: ${progressPct}%`}
181
- >
182
- <div
183
- className="h-full rounded-full bg-primary transition-all"
184
- style={{ width: `${progressPct}%` }}
185
- />
186
- </div>
187
- </div>
188
- </>
189
- )}
190
- </CardContent>
191
- </Card>
192
- </div>
193
- )
194
- }
195
-
196
- // ─────────────────────────────────────────────────────────────────────────────
197
- // Compliance tab
198
- // ─────────────────────────────────────────────────────────────────────────────
199
-
200
- const COMPLIANCE_ITEMS = [
201
- { label: "Background check", passed: true },
202
- { label: "Immunizations", passed: true },
203
- { label: "HIPAA training", passed: true },
204
- ]
205
-
206
- function ComplianceTab({ placement }: { placement: Placement }) {
207
- return (
208
- <div role="tabpanel" aria-label="Compliance">
209
- <Card>
210
- <CardHeader>
211
- <CardTitle className="text-base">Compliance Checklist</CardTitle>
212
- </CardHeader>
213
- <CardContent className="space-y-4">
214
- <ul className="space-y-3" aria-label="Compliance items">
215
- {COMPLIANCE_ITEMS.map((item) => (
216
- <li key={item.label} className="flex items-center gap-3 text-sm">
217
- {item.passed ? (
218
- <i
219
- className="fa-solid fa-circle-check text-emerald-600 text-base"
220
- aria-hidden="true"
221
- />
222
- ) : (
223
- <i
224
- className="fa-solid fa-circle-xmark text-red-500 text-base"
225
- aria-hidden="true"
226
- />
227
- )}
228
- <span>{item.label}</span>
229
- <span className="sr-only">
230
- {item.passed ? "completed" : "incomplete"}
231
- </span>
232
- </li>
233
- ))}
234
- </ul>
235
-
236
- <Separator />
237
-
238
- <dl>
239
- <InfoRow
240
- icon="fa-shield-check"
241
- label="Readiness"
242
- value={
243
- <Badge
244
- variant="outline"
245
- className={cn(
246
- "text-xs",
247
- placementReadinessBadgeClass(placement.readiness),
248
- )}
249
- >
250
- {placement.readiness}
251
- </Badge>
252
- }
253
- />
254
-
255
- {placement.placementPhase === "upcoming" &&
256
- placement.daysUntilStart > 0 && (
257
- <InfoRow
258
- icon="fa-calendar-days"
259
- label="Days until start"
260
- value={`${placement.daysUntilStart} days`}
261
- />
262
- )}
263
-
264
- <InfoRow
265
- icon="fa-clipboard-check"
266
- label="Compliance status"
267
- value={placement.compliance}
268
- />
269
- </dl>
270
- </CardContent>
271
- </Card>
272
- </div>
273
- )
274
- }
275
-
276
- // ─────────────────────────────────────────────────────────────────────────────
277
- // Activity tab
278
- // ─────────────────────────────────────────────────────────────────────────────
279
-
280
- const MOCK_ACTIVITY = [
281
- {
282
- date: "03/23/2026",
283
- description: "Supervisor evaluation submitted",
284
- icon: "fa-file-check",
285
- },
286
- {
287
- date: "03/22/2026",
288
- description: "Weekly check-in completed",
289
- icon: "fa-comments",
290
- },
291
- {
292
- date: "03/18/2026",
293
- description: "Hours log approved (20 hrs)",
294
- icon: "fa-clock",
295
- },
296
- {
297
- date: "03/15/2026",
298
- description: "Placement started",
299
- icon: "fa-play",
300
- },
301
- {
302
- date: "03/10/2026",
303
- description: "Compliance documents verified",
304
- icon: "fa-shield-check",
305
- },
306
- {
307
- date: "03/05/2026",
308
- description: "Placement confirmed by coordinator",
309
- icon: "fa-circle-check",
310
- },
311
- ]
312
-
313
- function ActivityTab() {
314
- return (
315
- <div role="tabpanel" aria-label="Activity">
316
- <Card>
317
- <CardHeader>
318
- <CardTitle className="text-base">Activity Timeline</CardTitle>
319
- </CardHeader>
320
- <CardContent>
321
- <ol className="relative border-s border-border ms-3" aria-label="Activity timeline">
322
- {MOCK_ACTIVITY.map((item, idx) => (
323
- <li key={idx} className="mb-6 ms-6 last:mb-0">
324
- <span className="absolute -start-3 flex size-6 items-center justify-center rounded-full bg-muted ring-4 ring-background">
325
- <i
326
- className={cn("fa-light", item.icon, "text-xs text-muted-foreground")}
327
- aria-hidden="true"
328
- />
329
- </span>
330
- <div className="flex flex-col gap-0.5">
331
- <time className="text-xs text-muted-foreground">{item.date}</time>
332
- <p className="text-sm text-foreground">{item.description}</p>
333
- </div>
334
- </li>
335
- ))}
336
- </ol>
337
- </CardContent>
338
- </Card>
339
- </div>
340
- )
341
- }
342
-
343
- // ─────────────────────────────────────────────────────────────────────────────
344
- // Main component
345
- // ─────────────────────────────────────────────────────────────────────────────
346
-
347
- export function PlacementDetail({ placement }: { placement: Placement }) {
348
- const [activeTab, setActiveTab] = useState<TabId>("overview")
349
-
350
- return (
351
- <div className="space-y-6">
352
- {/* Header */}
353
- <div className="flex items-start gap-4">
354
- <Avatar size="lg" className="size-12 shrink-0">
355
- <AvatarFallback className="text-sm font-bold bg-primary/10 text-primary">
356
- {placement.initials}
357
- </AvatarFallback>
358
- </Avatar>
359
- <div className="flex-1 min-w-0">
360
- <h1
361
- className="text-xl font-semibold"
362
- style={{ fontFamily: "var(--font-heading)" }}
363
- >
364
- {placement.student}
365
- </h1>
366
- <p className="text-sm text-muted-foreground">{placement.email}</p>
367
- <div className="flex items-center gap-2 mt-2 flex-wrap">
368
- <Badge variant="secondary">{placement.specialization}</Badge>
369
- <PlacementStatusBadge status={placement.status} />
370
- <Badge variant="outline">{placement.placementPhase}</Badge>
371
- </div>
372
- </div>
373
- <div className="flex items-center gap-2 shrink-0">
374
- <Tip label="Edit placement">
375
- <Button size="sm" variant="outline">
376
- <i className="fa-light fa-pen-to-square" aria-hidden="true" />{" "}
377
- Edit
378
- </Button>
379
- </Tip>
380
- <DropdownMenu>
381
- <DropdownMenuTrigger asChild>
382
- <Button size="sm" variant="outline" aria-label="More actions">
383
- <i className="fa-light fa-ellipsis" aria-hidden="true" />
384
- </Button>
385
- </DropdownMenuTrigger>
386
- <DropdownMenuContent align="end">
387
- <DropdownMenuItem>
388
- <i className="fa-light fa-download" aria-hidden="true" /> Export
389
- </DropdownMenuItem>
390
- <DropdownMenuItem>
391
- <i className="fa-light fa-box-archive" aria-hidden="true" />{" "}
392
- Archive
393
- </DropdownMenuItem>
394
- <DropdownMenuSeparator />
395
- <DropdownMenuItem className="text-destructive">
396
- <i className="fa-light fa-trash" aria-hidden="true" /> Delete
397
- </DropdownMenuItem>
398
- </DropdownMenuContent>
399
- </DropdownMenu>
400
- </div>
401
- </div>
402
-
403
- {/* Tab bar */}
404
- <div
405
- role="tablist"
406
- aria-label="Placement sections"
407
- className="inline-flex items-center gap-0.5 rounded-lg bg-muted/60 p-[3px]"
408
- >
409
- {TABS.map((tab) => (
410
- <button
411
- key={tab.id}
412
- role="tab"
413
- aria-selected={activeTab === tab.id}
414
- onClick={() => setActiveTab(tab.id)}
415
- className={cn(
416
- "px-3 py-1.5 text-xs rounded-md transition-all inline-flex items-center gap-1.5",
417
- activeTab === tab.id
418
- ? "bg-background text-foreground font-medium shadow-sm"
419
- : "text-muted-foreground hover:text-interactive-hover-foreground"
420
- )}
421
- >
422
- <i
423
- className={cn("fa-light", tab.icon, "text-xs")}
424
- aria-hidden="true"
425
- />{" "}
426
- {tab.label}
427
- </button>
428
- ))}
429
- </div>
430
-
431
- {/* Tab content */}
432
- {activeTab === "overview" && <OverviewTab placement={placement} />}
433
- {activeTab === "schedule" && <ScheduleTab placement={placement} />}
434
- {activeTab === "compliance" && <ComplianceTab placement={placement} />}
435
- {activeTab === "activity" && <ActivityTab />}
436
- </div>
437
- )
438
- }