@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,397 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * PlacementsBoardView — kanban-style board by placement phase (domain-specific columns).
5
- * View chrome labels use `dataListViewLabel` from `@/lib/data-list-view` at the page level;
6
- * this component focuses on placement phase grouping + shared card primitives.
7
- */
8
-
9
- import * as React from "react"
10
- import { useRouter } from "next/navigation"
11
- import { cn } from "@/lib/utils"
12
- import type { Placement, PlacementPhase } from "@/lib/mock/placements"
13
- import { Input } from "@/components/ui/input"
14
- import { Tip } from "@/components/ui/tip"
15
- import {
16
- DropdownMenu,
17
- DropdownMenuContent,
18
- DropdownMenuItem,
19
- DropdownMenuSeparator,
20
- DropdownMenuSub,
21
- DropdownMenuSubContent,
22
- DropdownMenuSubTrigger,
23
- DropdownMenuTrigger,
24
- } from "@/components/ui/dropdown-menu"
25
- import { DEFAULT_DATA_LIST_DISPLAY_OPTIONS, type BoardLineCount } from "@/lib/data-list-display-options"
26
- import type { ConditionalRule } from "@/components/table-properties/types"
27
- import type { ColumnDef } from "@/components/data-table/types"
28
- import { Badge } from "@/components/ui/badge"
29
- import { BoardPlacementCard } from "@/components/placement-board-card"
30
- import { BoardNewCardPlaceholder } from "@/components/data-views/board-card-primitives"
31
-
32
- const PHASE_COLUMNS: { phase: PlacementPhase; label: string; description: string }[] = [
33
- { phase: "upcoming", label: "Upcoming", description: "Starting soon" },
34
- { phase: "ongoing", label: "Ongoing", description: "In progress" },
35
- { phase: "completed", label: "Completed", description: "Finished" },
36
- ]
37
-
38
- /** Substring match across visible card fields (per-phase quick search). */
39
- function rowMatchesPhaseSearch(row: Placement, q: string): boolean {
40
- if (!q.trim()) return true
41
- const lower = q.toLowerCase()
42
- const hay = [
43
- row.student,
44
- row.site,
45
- row.specialization,
46
- row.internship,
47
- row.program,
48
- row.status,
49
- row.supervisor,
50
- row.email,
51
- row.start,
52
- ]
53
- .map(v => String(v ?? "").toLowerCase())
54
- .join(" ")
55
- return hay.includes(lower)
56
- }
57
-
58
- export interface PlacementsBoardColumnMenu {
59
- filterableColumns: { key: string; label: string }[]
60
- sortableColumns: { key: string; label: string }[]
61
- groupableColumns: { key: string; label: string }[]
62
- groupBy: string | null
63
- onAddFilter: (fieldKey: string) => void
64
- onSortByField: (fieldKey: string, direction: "asc" | "desc") => void
65
- onToggleGroupBy: (fieldKey: string) => void
66
- onOpenProperties: () => void
67
- }
68
-
69
- export interface BoardDisplaySettings {
70
- lineCount: BoardLineCount
71
- showColumnLabels: boolean
72
- showColumnCounts: boolean
73
- newCardAbove: boolean
74
- }
75
-
76
- export interface PlacementsBoardViewProps {
77
- placements: Placement[]
78
- /** When set, each phase column header shows the same actions as a DataTable column header. */
79
- boardColumnMenu?: PlacementsBoardColumnMenu
80
- /** Board display options (Properties → view display). */
81
- boardDisplay?: BoardDisplaySettings
82
- /** Column visibility from table state — hidden columns omit matching card fields. */
83
- hiddenColKeys?: Set<string>
84
- /** Same conditional formatting as the table (row background when a rule matches). */
85
- conditionalRules?: ConditionalRule[]
86
- /** Visible data columns (table order) — drives dates and other fields on the card. */
87
- boardColumns: ColumnDef<Placement>[]
88
- }
89
-
90
- function BoardPhaseColumnHeader({
91
- label,
92
- rawCount,
93
- filteredCount,
94
- searchValue,
95
- onSearchChange,
96
- menu,
97
- showLabels,
98
- showCounts,
99
- }: {
100
- label: string
101
- rawCount: number
102
- filteredCount: number
103
- searchValue: string
104
- onSearchChange: (value: string) => void
105
- menu: PlacementsBoardColumnMenu
106
- showLabels: boolean
107
- showCounts: boolean
108
- }) {
109
- const searchActive = Boolean(searchValue.trim())
110
- const countLabel =
111
- searchActive && filteredCount !== rawCount
112
- ? `${filteredCount} of ${rawCount} records`
113
- : `${filteredCount} ${filteredCount === 1 ? "record" : "records"}`
114
-
115
- const showLeft = showLabels || showCounts
116
-
117
- return (
118
- <div className="group/board-col border-b border-border px-3 py-2.5">
119
- <div className="flex items-center justify-between gap-2">
120
- {showLeft ? (
121
- <div className="flex min-w-0 flex-1 items-center gap-2">
122
- {showLabels ? (
123
- <p className="min-w-0 truncate text-sm font-semibold text-foreground">{label}</p>
124
- ) : null}
125
- {showCounts ? (
126
- <div className="flex shrink-0 items-center gap-1.5">
127
- <Badge
128
- variant="outline"
129
- className="inline-flex h-6 min-w-6 items-center justify-center border-0 bg-muted/70 px-2 text-xs font-semibold tabular-nums text-foreground"
130
- aria-label={countLabel}
131
- >
132
- {filteredCount}
133
- </Badge>
134
- {searchActive && filteredCount !== rawCount ? (
135
- <span className="text-xs font-medium tabular-nums text-muted-foreground" aria-hidden>
136
- / {rawCount}
137
- </span>
138
- ) : null}
139
- </div>
140
- ) : null}
141
- </div>
142
- ) : (
143
- <div className="min-w-0 flex-1" aria-hidden />
144
- )}
145
- <DropdownMenu>
146
- <Tip label="Column options" side="top">
147
- <DropdownMenuTrigger asChild>
148
- <button
149
- type="button"
150
- aria-label={`${label} column options`}
151
- onClick={e => e.stopPropagation()}
152
- className={cn(
153
- "opacity-0 group-hover/board-col:opacity-100 group-focus-within/board-col:opacity-100",
154
- "inline-flex shrink-0 items-center justify-center size-7 rounded-md",
155
- "text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover-row",
156
- "transition-opacity focus-visible:opacity-100",
157
- "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
158
- )}
159
- >
160
- <i className="fa-light fa-ellipsis-vertical text-xs" aria-hidden="true" />
161
- </button>
162
- </DropdownMenuTrigger>
163
- </Tip>
164
- <DropdownMenuContent align="end">
165
- <div className="px-2 pt-2 pb-1">
166
- <div className="relative">
167
- <i
168
- className="fa-light fa-magnifying-glass pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"
169
- aria-hidden="true"
170
- />
171
- <Input
172
- placeholder={`Search ${label}…`}
173
- value={searchValue}
174
- onChange={e => onSearchChange(e.target.value)}
175
- onKeyDown={e => e.stopPropagation()}
176
- className="h-7 ps-6 text-xs"
177
- />
178
- {searchValue ? (
179
- <button
180
- type="button"
181
- aria-label="Clear search"
182
- onClick={() => onSearchChange("")}
183
- className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-interactive-hover-foreground"
184
- >
185
- <i className="fa-light fa-xmark text-xs" aria-hidden="true" />
186
- </button>
187
- ) : null}
188
- </div>
189
- </div>
190
- <DropdownMenuSeparator />
191
-
192
- {menu.filterableColumns.length > 0 && (
193
- <>
194
- <DropdownMenuSub>
195
- <DropdownMenuSubTrigger>
196
- <i className="fa-light fa-filter" aria-hidden="true" />
197
- Filter by field…
198
- </DropdownMenuSubTrigger>
199
- <DropdownMenuSubContent className="max-h-[min(280px,60vh)] overflow-y-auto">
200
- {menu.filterableColumns.map(col => (
201
- <DropdownMenuItem key={col.key} onClick={() => menu.onAddFilter(col.key)}>
202
- {col.label}
203
- </DropdownMenuItem>
204
- ))}
205
- </DropdownMenuSubContent>
206
- </DropdownMenuSub>
207
- <DropdownMenuSeparator />
208
- </>
209
- )}
210
-
211
- {menu.sortableColumns.length > 0 && (
212
- <>
213
- <DropdownMenuSub>
214
- <DropdownMenuSubTrigger>
215
- <i className="fa-light fa-arrow-up-arrow-down" aria-hidden="true" />
216
- Sort by…
217
- </DropdownMenuSubTrigger>
218
- <DropdownMenuSubContent className="max-h-[min(320px,60vh)] overflow-y-auto">
219
- {menu.sortableColumns.map(col => (
220
- <React.Fragment key={col.key}>
221
- <DropdownMenuItem onClick={() => menu.onSortByField(col.key, "asc")}>
222
- <i className="fa-light fa-arrow-up-a-z" aria-hidden="true" />
223
- {col.label} — ascending
224
- </DropdownMenuItem>
225
- <DropdownMenuItem onClick={() => menu.onSortByField(col.key, "desc")}>
226
- <i className="fa-light fa-arrow-down-a-z" aria-hidden="true" />
227
- {col.label} — descending
228
- </DropdownMenuItem>
229
- </React.Fragment>
230
- ))}
231
- </DropdownMenuSubContent>
232
- </DropdownMenuSub>
233
- <DropdownMenuSeparator />
234
- </>
235
- )}
236
-
237
- {menu.groupableColumns.length > 0 && (
238
- <>
239
- <DropdownMenuSub>
240
- <DropdownMenuSubTrigger>
241
- <i className="fa-light fa-layer-group" aria-hidden="true" />
242
- Group by…
243
- </DropdownMenuSubTrigger>
244
- <DropdownMenuSubContent className="max-h-[min(280px,60vh)] overflow-y-auto">
245
- {menu.groupableColumns.map(col => (
246
- <DropdownMenuItem
247
- key={col.key}
248
- onClick={() => menu.onToggleGroupBy(col.key)}
249
- >
250
- {menu.groupBy === col.key ? (
251
- <>
252
- <i className="fa-light fa-check text-xs" aria-hidden="true" />
253
- Grouped by {col.label}
254
- </>
255
- ) : (
256
- <>
257
- <span className="inline-block w-3" aria-hidden />
258
- Group by {col.label}
259
- </>
260
- )}
261
- </DropdownMenuItem>
262
- ))}
263
- </DropdownMenuSubContent>
264
- </DropdownMenuSub>
265
- <DropdownMenuSeparator />
266
- </>
267
- )}
268
-
269
- <DropdownMenuItem onClick={menu.onOpenProperties}>
270
- <i className="fa-light fa-palette" aria-hidden="true" />
271
- Add conditional rule
272
- </DropdownMenuItem>
273
- </DropdownMenuContent>
274
- </DropdownMenu>
275
- </div>
276
- </div>
277
- )
278
- }
279
-
280
- export function PlacementsBoardView({
281
- placements,
282
- boardColumnMenu,
283
- boardDisplay: boardDisplayProp,
284
- hiddenColKeys: hiddenColKeysProp,
285
- conditionalRules,
286
- boardColumns,
287
- }: PlacementsBoardViewProps) {
288
- const router = useRouter()
289
-
290
- const bd: BoardDisplaySettings = {
291
- lineCount: boardDisplayProp?.lineCount ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.boardLineCount,
292
- showColumnLabels: boardDisplayProp?.showColumnLabels ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.showColumnLabels,
293
- showColumnCounts: boardDisplayProp?.showColumnCounts ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.showBoardColumnCounts,
294
- newCardAbove: boardDisplayProp?.newCardAbove ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.boardNewCardAbove,
295
- }
296
- const hiddenColKeys = hiddenColKeysProp ?? new Set<string>()
297
-
298
- const [phaseSearch, setPhaseSearch] = React.useState<Record<PlacementPhase, string>>({
299
- upcoming: "",
300
- ongoing: "",
301
- completed: "",
302
- })
303
-
304
- const byPhase = React.useMemo(() => {
305
- const map: Record<PlacementPhase, Placement[]> = {
306
- upcoming: [],
307
- ongoing: [],
308
- completed: [],
309
- }
310
- for (const p of placements) {
311
- map[p.placementPhase].push(p)
312
- }
313
- return map
314
- }, [placements])
315
-
316
- const cardsByPhase = React.useMemo(() => {
317
- const out: Record<PlacementPhase, Placement[]> = {
318
- upcoming: [],
319
- ongoing: [],
320
- completed: [],
321
- }
322
- for (const phase of PHASE_COLUMNS.map(c => c.phase)) {
323
- const q = phaseSearch[phase]
324
- out[phase] = byPhase[phase].filter(row => rowMatchesPhaseSearch(row, q))
325
- }
326
- return out
327
- }, [byPhase, phaseSearch])
328
-
329
- return (
330
- <div className="px-4 pb-8 pt-2 lg:px-6">
331
- <p className="text-xs text-muted-foreground mb-4">
332
- Rows grouped by phase (same data as Table view and List view).
333
- </p>
334
- <div className="grid grid-cols-1 gap-4 md:grid-cols-3 min-h-[min(480px,calc(100vh-14rem))]">
335
- {PHASE_COLUMNS.map(col => {
336
- const rawInPhase = byPhase[col.phase]
337
- const cards = cardsByPhase[col.phase]
338
-
339
- return (
340
- <div
341
- key={col.phase}
342
- className="group/board-col flex min-h-0 flex-col rounded-xl border border-border bg-muted/30"
343
- >
344
- {boardColumnMenu ? (
345
- <BoardPhaseColumnHeader
346
- label={col.label}
347
- rawCount={rawInPhase.length}
348
- filteredCount={cards.length}
349
- searchValue={phaseSearch[col.phase]}
350
- onSearchChange={v => setPhaseSearch(prev => ({ ...prev, [col.phase]: v }))}
351
- menu={boardColumnMenu}
352
- showLabels={bd.showColumnLabels}
353
- showCounts={bd.showColumnCounts}
354
- />
355
- ) : (
356
- <div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2.5">
357
- {bd.showColumnLabels ? (
358
- <p className="min-w-0 truncate text-sm font-semibold text-foreground">{col.label}</p>
359
- ) : (
360
- <span className="min-w-0 flex-1" aria-hidden />
361
- )}
362
- {bd.showColumnCounts ? (
363
- <Badge
364
- variant="outline"
365
- className="inline-flex h-6 min-w-6 shrink-0 items-center justify-center border-0 bg-muted/70 px-2 text-xs font-semibold tabular-nums text-foreground"
366
- aria-label={`${rawInPhase.length} ${rawInPhase.length === 1 ? "record" : "records"}`}
367
- >
368
- {rawInPhase.length}
369
- </Badge>
370
- ) : null}
371
- </div>
372
- )}
373
- <div className="flex flex-1 flex-col gap-2 overflow-y-auto p-2">
374
- {bd.newCardAbove ? <BoardNewCardPlaceholder position="above" /> : null}
375
- {cards.length === 0 ? (
376
- <p className="px-2 py-6 text-center text-xs text-muted-foreground">No placements</p>
377
- ) : (
378
- cards.map(row => (
379
- <BoardPlacementCard
380
- key={row.id}
381
- row={row}
382
- hiddenColKeys={hiddenColKeys}
383
- lineCount={bd.lineCount}
384
- conditionalRules={conditionalRules}
385
- boardColumns={boardColumns}
386
- onOpen={id => router.push(`/data-list/${id}`)}
387
- />
388
- ))
389
- )}
390
- </div>
391
- </div>
392
- )
393
- })}
394
- </div>
395
- </div>
396
- )
397
- }
@@ -1,220 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * PlacementsClient — placements hub composition on the reusable
5
- * `ListPageTemplate`. Owns the per-page persisted layout (tabs, display
6
- * options, show-metrics toggle) and mounts `PlacementsTable` per tab.
7
- *
8
- * Uses centralized exports from `@/components/data-views`.
9
- *
10
- * NOTE: Lifecycle (all/upcoming/ongoing/completed) segments have been removed.
11
- * All tabs read the same row bag; users can create their own segments via
12
- * "Add view" / filters.
13
- */
14
-
15
- import * as React from "react"
16
- import { useRouter } from "next/navigation"
17
- import { useSidebar } from "@/components/ui/sidebar"
18
- import {
19
- ListPageTemplate,
20
- type ViewTab,
21
- PlacementsTable,
22
- type PlacementsTableHandle,
23
- type DataListViewType,
24
- dataListViewIcon,
25
- } from "@/components/data-views"
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 { ALL_PLACEMENTS } 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 move between saved views. Each tab keeps its own layout, filters, and properties.",
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
- ]
107
-
108
- // ─────────────────────────────────────────────────────────────────────────────
109
- // Component
110
- // ─────────────────────────────────────────────────────────────────────────────
111
-
112
- export function PlacementsClient() {
113
- const router = useRouter()
114
- const { setOpen } = useSidebar()
115
- const [showMetrics, setShowMetrics] = React.useState(true)
116
- const [exportOpen, setExportOpen] = React.useState(false)
117
- const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
118
- const [tabs, setTabs] = React.useState<ViewTab[]>(DEFAULT_TABS)
119
- const [activeTabId, setActiveTabId] = React.useState<string>(DEFAULT_TABS[0]?.id ?? "")
120
- const tableRef = React.useRef<PlacementsTableHandle>(null)
121
-
122
- const viewsTour = useCoachMark({
123
- flowId: "data-list-views-tour",
124
- steps: VIEWS_TOUR_STEPS,
125
- delay: 1200,
126
- })
127
-
128
- const activeTab = tabs.find((t) => t.id === activeTabId)
129
- const placementCount = ALL_PLACEMENTS.length
130
-
131
- useAskLeoPageContext(
132
- React.useMemo(
133
- () => ({
134
- title: "List hub",
135
- description: activeTab
136
- ? `${placementCount} row${placementCount === 1 ? "" : "s"} in “${activeTab.label}” · ${activeTab.viewType} view.`
137
- : undefined,
138
- suggestions: [
139
- "Which rows are due in the next 30 days?",
140
- "Summarize what is visible after my filters",
141
- "What columns help reviewers scan this grid quickly?",
142
- ],
143
- }),
144
- [activeTab, placementCount],
145
- ),
146
- )
147
-
148
- React.useLayoutEffect(() => {
149
- const p = loadPageFromStorage()
150
- if (!p) return
151
- setDisplayOptions({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...p.displayOptions })
152
- setShowMetrics(p.showMetrics)
153
- setTabs(p.tabs)
154
- const nextActive = p.tabs.some(t => t.id === p.activeTabId) ? p.activeTabId : (p.tabs[0]?.id ?? "")
155
- setActiveTabId(nextActive)
156
- }, [])
157
-
158
- React.useEffect(() => {
159
- schedulePageSave({
160
- v: 1,
161
- displayOptions,
162
- showMetrics,
163
- tabs,
164
- activeTabId,
165
- })
166
- }, [displayOptions, showMetrics, tabs, activeTabId])
167
-
168
- function handleNewPlacement() {
169
- setOpen(false)
170
- setTimeout(() => router.push("/data-list/new"), 260)
171
- }
172
-
173
- return (
174
- <>
175
- <CoachMark state={viewsTour} />
176
- <div className="flex min-h-0 flex-1 flex-col">
177
- <ListPageTemplate
178
- tabs={tabs}
179
- onTabsChange={setTabs}
180
- activeTabId={activeTabId}
181
- onActiveTabChange={setActiveTabId}
182
- tablePropertiesRef={tableRef}
183
- header={
184
- <PlacementsPageHeader
185
- onNewPlacement={handleNewPlacement}
186
- onExport={() => setExportOpen(true)}
187
- showMetrics={showMetrics}
188
- onToggleMetrics={() => setShowMetrics(v => !v)}
189
- showTitleBlock={displayOptions.showViewTitle}
190
- />
191
- }
192
- metrics={
193
- <KeyMetrics
194
- variant="flat"
195
- metrics={PLACEMENT_KPI_METRICS}
196
- insight={PLACEMENT_KPI_INSIGHT}
197
- showHeader={false}
198
- metricsSingleRow
199
- />
200
- }
201
- showMetrics={showMetrics}
202
- defaultTabs={DEFAULT_TABS}
203
- renderContent={(tab, updateTab) => (
204
- <PlacementsTable
205
- key={tab.id}
206
- ref={tableRef}
207
- view={tab.viewType}
208
- onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
209
- displayOptions={displayOptions}
210
- onDisplayOptionsChange={patch =>
211
- setDisplayOptions(prev => ({ ...prev, ...patch }))}
212
- />
213
- )}
214
- exportOpen={exportOpen}
215
- onExportOpenChange={setExportOpen}
216
- />
217
- </div>
218
- </>
219
- )
220
- }