@exxatdesignux/ui 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/bin/init.mjs +29 -0
  2. package/package.json +7 -2
  3. package/template/.nvmrc +1 -0
  4. package/template/.prettierignore +7 -0
  5. package/template/.prettierrc +11 -0
  6. package/template/AGENTS.md +485 -0
  7. package/template/Logo/Exxat_Prism.svg +39 -0
  8. package/template/Logo/Exxat_one.svg +36 -0
  9. package/template/README.md +58 -0
  10. package/template/app/(app)/compliance/page.tsx +10 -0
  11. package/template/app/(app)/dashboard/loading.tsx +18 -0
  12. package/template/app/(app)/dashboard/page.tsx +36 -0
  13. package/template/app/(app)/data-list/[id]/page.tsx +28 -0
  14. package/template/app/(app)/data-list/new/page.tsx +31 -0
  15. package/template/app/(app)/data-list/page.tsx +10 -0
  16. package/template/app/(app)/error.tsx +43 -0
  17. package/template/app/(app)/help/page.tsx +34 -0
  18. package/template/app/(app)/layout.tsx +54 -0
  19. package/template/app/(app)/loading.tsx +18 -0
  20. package/template/app/(app)/question-bank/page.tsx +10 -0
  21. package/template/app/(app)/rotations/page.tsx +15 -0
  22. package/template/app/(app)/settings/page.tsx +17 -0
  23. package/template/app/(app)/sites/all/page.tsx +13 -0
  24. package/template/app/(app)/team/page.tsx +10 -0
  25. package/template/app/favicon.ico +0 -0
  26. package/template/app/globals.css +1811 -0
  27. package/template/app/layout.tsx +95 -0
  28. package/template/app/page.tsx +9 -0
  29. package/template/components/.gitkeep +0 -0
  30. package/template/components/app-sidebar-dynamic.tsx +15 -0
  31. package/template/components/app-sidebar.tsx +901 -0
  32. package/template/components/ask-leo-composer.tsx +216 -0
  33. package/template/components/ask-leo-sidebar.tsx +509 -0
  34. package/template/components/chart-area-interactive.tsx +293 -0
  35. package/template/components/charts-overview.tsx +2321 -0
  36. package/template/components/command-menu-01.tsx +133 -0
  37. package/template/components/command-menu-02.tsx +386 -0
  38. package/template/components/command-menu.tsx +182 -0
  39. package/template/components/compliance-board-view.tsx +134 -0
  40. package/template/components/compliance-client.tsx +92 -0
  41. package/template/components/compliance-list-view.tsx +59 -0
  42. package/template/components/compliance-page-header.tsx +89 -0
  43. package/template/components/compliance-table.tsx +525 -0
  44. package/template/components/dashboard-onboarding-gallery.tsx +13 -0
  45. package/template/components/dashboard-onboarding.tsx +21 -0
  46. package/template/components/dashboard-promo-banner.tsx +67 -0
  47. package/template/components/dashboard-quota-progress-card.tsx +369 -0
  48. package/template/components/dashboard-report-charts.tsx +69 -0
  49. package/template/components/dashboard-section-heading.tsx +68 -0
  50. package/template/components/dashboard-tabs.tsx +598 -0
  51. package/template/components/data-list-client.tsx +239 -0
  52. package/template/components/data-list-table-cells.test.tsx +22 -0
  53. package/template/components/data-list-table-cells.tsx +173 -0
  54. package/template/components/data-list-table.tsx +879 -0
  55. package/template/components/data-table/filter-date-calendar.tsx +38 -0
  56. package/template/components/data-table/filter-text-value-input.tsx +77 -0
  57. package/template/components/data-table/index.tsx +1612 -0
  58. package/template/components/data-table/pagination.tsx +256 -0
  59. package/template/components/data-table/types.ts +91 -0
  60. package/template/components/data-table/use-table-state.ts +566 -0
  61. package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
  62. package/template/components/data-view-dashboard-charts-team.tsx +968 -0
  63. package/template/components/data-view-dashboard-charts.tsx +1668 -0
  64. package/template/components/data-views/board-card-primitives.tsx +93 -0
  65. package/template/components/data-views/index.ts +41 -0
  66. package/template/components/data-views/list-page-board-card.tsx +192 -0
  67. package/template/components/data-views/list-page-board-template.tsx +122 -0
  68. package/template/components/data-views/placement-board-card.tsx +262 -0
  69. package/template/components/export-drawer.tsx +375 -0
  70. package/template/components/exxat-product-logo.tsx +453 -0
  71. package/template/components/form-layout-01.tsx +131 -0
  72. package/template/components/getting-started.tsx +625 -0
  73. package/template/components/key-metrics.tsx +920 -0
  74. package/template/components/leo-insight-indicator.tsx +364 -0
  75. package/template/components/leo-typing-dots.tsx +121 -0
  76. package/template/components/list-hub-status-badge.tsx +51 -0
  77. package/template/components/list-page-dashboard-charts.tsx +18 -0
  78. package/template/components/nav-documents.tsx +89 -0
  79. package/template/components/nav-main.tsx +58 -0
  80. package/template/components/nav-secondary.tsx +64 -0
  81. package/template/components/nav-user.tsx +190 -0
  82. package/template/components/new-placement-back-btn.tsx +28 -0
  83. package/template/components/new-placement-form.tsx +1066 -0
  84. package/template/components/onboarding/index.ts +4 -0
  85. package/template/components/onboarding/onboarding-01.tsx +7 -0
  86. package/template/components/onboarding/onboarding-02.tsx +7 -0
  87. package/template/components/onboarding/onboarding-03.tsx +7 -0
  88. package/template/components/onboarding/onboarding-04.tsx +7 -0
  89. package/template/components/page-header.tsx +57 -0
  90. package/template/components/placement-detail.tsx +438 -0
  91. package/template/components/placements-board-view.tsx +404 -0
  92. package/template/components/placements-list-view.tsx +285 -0
  93. package/template/components/placements-page-header.tsx +160 -0
  94. package/template/components/placements-table-columns.tsx +639 -0
  95. package/template/components/product-switcher.tsx +116 -0
  96. package/template/components/question-bank-board-view.tsx +205 -0
  97. package/template/components/question-bank-client.tsx +77 -0
  98. package/template/components/question-bank-list-view.tsx +59 -0
  99. package/template/components/question-bank-page-header.tsx +89 -0
  100. package/template/components/question-bank-table.tsx +586 -0
  101. package/template/components/rotations-empty-state.tsx +47 -0
  102. package/template/components/rotations-panel-activator.tsx +8 -0
  103. package/template/components/secondary-nav.tsx +394 -0
  104. package/template/components/secondary-panel.tsx +239 -0
  105. package/template/components/section-cards.tsx +106 -0
  106. package/template/components/settings-appearance-card.tsx +424 -0
  107. package/template/components/settings-client.tsx +537 -0
  108. package/template/components/settings-form-row.tsx +42 -0
  109. package/template/components/sidebar-auto-collapse.tsx +23 -0
  110. package/template/components/sidebar-auto-open.tsx +18 -0
  111. package/template/components/sidebar-shell.tsx +37 -0
  112. package/template/components/site-header.tsx +93 -0
  113. package/template/components/sites-all-client.tsx +154 -0
  114. package/template/components/sites-board-view.tsx +67 -0
  115. package/template/components/sites-list-view.tsx +47 -0
  116. package/template/components/sites-table.tsx +312 -0
  117. package/template/components/system-banner-slot.tsx +66 -0
  118. package/template/components/table-properties/column-row.tsx +90 -0
  119. package/template/components/table-properties/draggable-list.ts +49 -0
  120. package/template/components/table-properties/drawer-button.tsx +231 -0
  121. package/template/components/table-properties/drawer.tsx +1102 -0
  122. package/template/components/table-properties/filter-card.tsx +251 -0
  123. package/template/components/table-properties/index.ts +22 -0
  124. package/template/components/table-properties/sort-card.tsx +59 -0
  125. package/template/components/table-properties/types.ts +124 -0
  126. package/template/components/task-list-panel.tsx +98 -0
  127. package/template/components/task-priority-badge.tsx +28 -0
  128. package/template/components/team-board-view.tsx +114 -0
  129. package/template/components/team-client.tsx +93 -0
  130. package/template/components/team-list-view.tsx +62 -0
  131. package/template/components/team-page-header.tsx +92 -0
  132. package/template/components/team-table.tsx +525 -0
  133. package/template/components/templates/list-page.tsx +576 -0
  134. package/template/components/templates/primary-page-template.tsx +56 -0
  135. package/template/components/theme-color-sync.tsx +32 -0
  136. package/template/components/theme-provider.tsx +71 -0
  137. package/template/components/tinted-icon-disc.tsx +53 -0
  138. package/template/components/ui/ai-thinking-surface.tsx +121 -0
  139. package/template/components/ui/avatar.tsx +1 -0
  140. package/template/components/ui/badge.tsx +1 -0
  141. package/template/components/ui/banner.tsx +1 -0
  142. package/template/components/ui/breadcrumb.tsx +1 -0
  143. package/template/components/ui/button.tsx +1 -0
  144. package/template/components/ui/calendar.tsx +1 -0
  145. package/template/components/ui/card.tsx +1 -0
  146. package/template/components/ui/chart.tsx +1 -0
  147. package/template/components/ui/checkbox.tsx +1 -0
  148. package/template/components/ui/coach-mark.tsx +1 -0
  149. package/template/components/ui/collapsible.tsx +1 -0
  150. package/template/components/ui/command.tsx +1 -0
  151. package/template/components/ui/date-picker-field.tsx +1 -0
  152. package/template/components/ui/dialog.tsx +1 -0
  153. package/template/components/ui/dot-pattern.tsx +159 -0
  154. package/template/components/ui/drag-handle-grip.tsx +1 -0
  155. package/template/components/ui/drawer.tsx +1 -0
  156. package/template/components/ui/dropdown-menu.tsx +1 -0
  157. package/template/components/ui/field.tsx +1 -0
  158. package/template/components/ui/form.tsx +1 -0
  159. package/template/components/ui/input-group.tsx +1 -0
  160. package/template/components/ui/input-mask.tsx +1 -0
  161. package/template/components/ui/input.tsx +1 -0
  162. package/template/components/ui/kbd.tsx +1 -0
  163. package/template/components/ui/label.tsx +1 -0
  164. package/template/components/ui/leo-icon.tsx +726 -0
  165. package/template/components/ui/payment-card-fields.tsx +1 -0
  166. package/template/components/ui/popover.tsx +1 -0
  167. package/template/components/ui/radio-group.tsx +1 -0
  168. package/template/components/ui/select.tsx +1 -0
  169. package/template/components/ui/selection-tile-grid.tsx +1 -0
  170. package/template/components/ui/separator.tsx +1 -0
  171. package/template/components/ui/sheet.tsx +1 -0
  172. package/template/components/ui/sidebar.tsx +1 -0
  173. package/template/components/ui/skeleton.tsx +1 -0
  174. package/template/components/ui/sonner.tsx +1 -0
  175. package/template/components/ui/status-badge.tsx +1 -0
  176. package/template/components/ui/table.tsx +1 -0
  177. package/template/components/ui/tabs.tsx +1 -0
  178. package/template/components/ui/textarea.tsx +1 -0
  179. package/template/components/ui/tip.tsx +1 -0
  180. package/template/components/ui/toggle-group.tsx +1 -0
  181. package/template/components/ui/toggle-switch.tsx +1 -0
  182. package/template/components/ui/toggle.tsx +1 -0
  183. package/template/components/ui/tooltip.tsx +1 -0
  184. package/template/components/ui/view-segmented-control.tsx +1 -0
  185. package/template/components.json +27 -0
  186. package/template/contexts/chart-variant-context.tsx +35 -0
  187. package/template/contexts/command-menu-context.tsx +28 -0
  188. package/template/contexts/dashboard-view-context.tsx +35 -0
  189. package/template/contexts/product-context.tsx +38 -0
  190. package/template/contexts/system-banner-context.tsx +127 -0
  191. package/template/docs/command-menu-pattern.md +45 -0
  192. package/template/docs/data-views-pattern.md +160 -0
  193. package/template/ecosystem.config.cjs +20 -0
  194. package/template/eslint.config.mjs +18 -0
  195. package/template/fontawesome-subset.manifest.json +190 -0
  196. package/template/hooks/.gitkeep +0 -0
  197. package/template/hooks/use-app-theme.ts +1 -0
  198. package/template/hooks/use-coach-mark.ts +1 -0
  199. package/template/hooks/use-mobile.ts +1 -0
  200. package/template/hooks/use-mod-key-label.ts +1 -0
  201. package/template/lib/.gitkeep +0 -0
  202. package/template/lib/ask-leo-route-context.ts +133 -0
  203. package/template/lib/chart-keyboard-selection.test.ts +20 -0
  204. package/template/lib/chart-keyboard-selection.ts +17 -0
  205. package/template/lib/chart-line-dash.ts +16 -0
  206. package/template/lib/coach-mark-registry.ts +68 -0
  207. package/template/lib/command-menu-config.ts +127 -0
  208. package/template/lib/command-menu-search-data.ts +44 -0
  209. package/template/lib/conditional-rule-match.ts +32 -0
  210. package/template/lib/dashboard-customize-coach-mark.ts +18 -0
  211. package/template/lib/dashboard-layout-merge.ts +63 -0
  212. package/template/lib/data-list-display-options.ts +35 -0
  213. package/template/lib/data-list-persistence.ts +280 -0
  214. package/template/lib/data-list-view-surface.ts +58 -0
  215. package/template/lib/data-list-view.ts +29 -0
  216. package/template/lib/data-view-dashboard-storage.ts +101 -0
  217. package/template/lib/date-filter.ts +8 -0
  218. package/template/lib/dev-log.test.ts +28 -0
  219. package/template/lib/dev-log.ts +8 -0
  220. package/template/lib/editable-target.ts +10 -0
  221. package/template/lib/floating-sheet-panel.ts +72 -0
  222. package/template/lib/initials-from-name.ts +7 -0
  223. package/template/lib/list-page-table-properties.ts +52 -0
  224. package/template/lib/list-status-badges.ts +168 -0
  225. package/template/lib/logo-dev.ts +12 -0
  226. package/template/lib/mock/compliance-kpi.ts +61 -0
  227. package/template/lib/mock/compliance.ts +146 -0
  228. package/template/lib/mock/dashboard.ts +105 -0
  229. package/template/lib/mock/navigation.tsx +231 -0
  230. package/template/lib/mock/placements-kpi.ts +134 -0
  231. package/template/lib/mock/placements.ts +183 -0
  232. package/template/lib/mock/question-bank-kpi.ts +61 -0
  233. package/template/lib/mock/question-bank.ts +142 -0
  234. package/template/lib/mock/sites-directory.ts +16 -0
  235. package/template/lib/mock/sites-kpi.ts +25 -0
  236. package/template/lib/mock/team-kpi.ts +60 -0
  237. package/template/lib/mock/team.ts +118 -0
  238. package/template/lib/motion-ui.ts +17 -0
  239. package/template/lib/placement-board-card-layout.ts +79 -0
  240. package/template/lib/placement-lifecycle.ts +5 -0
  241. package/template/lib/row-height.ts +10 -0
  242. package/template/lib/stock-portrait.ts +11 -0
  243. package/template/lib/utils.test.ts +13 -0
  244. package/template/lib/utils.ts +1 -0
  245. package/template/next.config.mjs +15 -0
  246. package/template/package.json +83 -0
  247. package/template/postcss.config.mjs +8 -0
  248. package/template/public/.gitkeep +0 -0
  249. package/template/public/Illustration/Rotation.svg +74 -0
  250. package/template/public/avatars/user.svg +11 -0
  251. package/template/public/favicon/favicon.ico +0 -0
  252. package/template/public/favicon.ico +0 -0
  253. package/template/public/logos/exxat-one.svg +36 -0
  254. package/template/public/logos/exxat-prism.svg +39 -0
  255. package/template/public/mock-schools/emory.svg +4 -0
  256. package/template/public/mock-schools/rush.svg +4 -0
  257. package/template/scripts/fontawesome-subset-audit.mjs +190 -0
  258. package/template/scripts/pm2-startup-macos.sh +13 -0
  259. package/template/skills-lock.json +10 -0
  260. package/template/stores/app-store.ts +33 -0
  261. package/template/tests/setup.ts +1 -0
  262. package/template/tsconfig.json +35 -0
  263. package/template/types/react-payment-inputs.d.ts +19 -0
  264. package/template/vitest.config.ts +18 -0
@@ -0,0 +1,586 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Question bank — DataTable + TablePropertiesDrawer + list/board/dashboard (KPI + charts on dashboard).
5
+ */
6
+
7
+ import * as React from "react"
8
+ import { Bar, BarChart, CartesianGrid, Cell, XAxis, YAxis } from "recharts"
9
+ import { ChartCard, ChartDataTable, ChartFigure } from "@/components/charts-overview"
10
+ import { DataTable, DataTableToolbar } from "@/components/data-table"
11
+ import type { DataListViewType } from "@/lib/data-list-view"
12
+ import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
13
+ import type { ColumnDef } from "@/components/data-table/types"
14
+ import { useTableState } from "@/components/data-table/use-table-state"
15
+ import { TablePropertiesDrawerButton } from "@/components/table-properties"
16
+ import type { ConditionalRule, FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
17
+ import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
18
+ import { Button } from "@/components/ui/button"
19
+ import {
20
+ ChartContainer,
21
+ ChartTooltip,
22
+ chartTooltipKeyboardSyncProps,
23
+ ChartTooltipContent,
24
+ type ChartConfig,
25
+ } from "@/components/ui/chart"
26
+ import {
27
+ DropdownMenu,
28
+ DropdownMenuContent,
29
+ DropdownMenuItem,
30
+ DropdownMenuTrigger,
31
+ } from "@/components/ui/dropdown-menu"
32
+ import { Tip } from "@/components/ui/tip"
33
+ import { KeyMetrics } from "@/components/key-metrics"
34
+ import { QuestionBankBoardView, QUESTION_BANK_BOARD_GROUP_OPTIONS } from "@/components/question-bank-board-view"
35
+ import { QuestionBankListView } from "@/components/question-bank-list-view"
36
+ import { CHART_KBD_ACTIVE_BAR } from "@/lib/chart-keyboard-selection"
37
+ import type { QuestionBankDifficulty, QuestionBankItem, QuestionBankType } from "@/lib/mock/question-bank"
38
+ import { questionBankKpiInsight, questionBankKpiMetrics } from "@/lib/mock/question-bank-kpi"
39
+ import {
40
+ QUESTION_BANK_STATUS_BADGE_CLASS,
41
+ QUESTION_BANK_STATUS_ICON,
42
+ QUESTION_BANK_STATUS_LABEL,
43
+ } from "@/lib/list-status-badges"
44
+ import {
45
+ DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
46
+ type DataListDisplayOptions,
47
+ } from "@/lib/data-list-display-options"
48
+
49
+ const TYPE_LABEL: Record<QuestionBankType, string> = {
50
+ multiple_choice: "Multiple choice",
51
+ true_false: "True / false",
52
+ short_answer: "Short answer",
53
+ }
54
+
55
+ const DIFFICULTY_LABEL: Record<QuestionBankDifficulty, string> = {
56
+ easy: "Easy",
57
+ medium: "Medium",
58
+ hard: "Hard",
59
+ }
60
+
61
+ const BAR_CFG: ChartConfig = {
62
+ count: { label: "Questions", color: "var(--color-chart-2)" },
63
+ }
64
+
65
+ function uniqueTopics(items: QuestionBankItem[]) {
66
+ return [...new Set(items.map(i => i.topic))].sort().map(t => ({ value: t, label: t }))
67
+ }
68
+
69
+ const STATUS_FILTER_OPTS = [
70
+ { value: "published", label: QUESTION_BANK_STATUS_LABEL.published },
71
+ { value: "draft", label: QUESTION_BANK_STATUS_LABEL.draft },
72
+ { value: "in_review", label: QUESTION_BANK_STATUS_LABEL.in_review },
73
+ ]
74
+
75
+ const TYPE_FILTER_OPTS = (Object.keys(TYPE_LABEL) as QuestionBankType[]).map(k => ({
76
+ value: k,
77
+ label: TYPE_LABEL[k],
78
+ }))
79
+
80
+ const DIFFICULTY_FILTER_OPTS = (Object.keys(DIFFICULTY_LABEL) as QuestionBankDifficulty[]).map(k => ({
81
+ value: k,
82
+ label: DIFFICULTY_LABEL[k],
83
+ }))
84
+
85
+ function columnToFilterFieldDef(c: ColumnDef<QuestionBankItem>): FilterFieldDef | null {
86
+ if (!c.filter) return null
87
+ const f = c.filter
88
+ const defaultOps: FilterOperator[] =
89
+ f.type === "select" || f.type === "date"
90
+ ? ["is", "is_not"]
91
+ : ["contains", "not_contains"]
92
+ return {
93
+ key: c.key,
94
+ label: c.label,
95
+ icon: f.icon ?? "fa-filter",
96
+ type: f.type,
97
+ operators: (f.operators ?? defaultOps) as FilterOperator[],
98
+ options: f.options,
99
+ ...(f.textMask ? { textMask: f.textMask } : {}),
100
+ }
101
+ }
102
+
103
+ function columnsToFilterFields(cols: ColumnDef<QuestionBankItem>[]) {
104
+ return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
105
+ }
106
+
107
+ function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<QuestionBankItem>[] {
108
+ const topicOpts = uniqueTopics(items)
109
+
110
+ const COLUMN_SELECT: ColumnDef<QuestionBankItem> = {
111
+ key: "select",
112
+ label: "",
113
+ width: 40,
114
+ minWidth: 40,
115
+ defaultPin: "left",
116
+ lockPin: true,
117
+ }
118
+
119
+ const cols: ColumnDef<QuestionBankItem>[] = [
120
+ COLUMN_SELECT,
121
+ {
122
+ key: "stem",
123
+ label: "Question",
124
+ width: 300,
125
+ minWidth: 160,
126
+ sortable: true,
127
+ sortKey: "stem",
128
+ defaultPin: "left",
129
+ filter: {
130
+ type: "text",
131
+ icon: "fa-file-lines",
132
+ operators: ["contains", "not_contains"],
133
+ },
134
+ cell: row => (
135
+ <span className="line-clamp-2 text-sm font-medium text-foreground">{row.stem}</span>
136
+ ),
137
+ },
138
+ {
139
+ key: "topic",
140
+ label: "Topic",
141
+ width: 160,
142
+ minWidth: 120,
143
+ sortable: true,
144
+ sortKey: "topic",
145
+ filter: {
146
+ type: "select",
147
+ icon: "fa-layer-group",
148
+ operators: ["is", "is_not"],
149
+ options: topicOpts,
150
+ },
151
+ cell: row => <span className="text-sm text-foreground/90">{row.topic}</span>,
152
+ },
153
+ {
154
+ key: "type",
155
+ label: "Type",
156
+ width: 140,
157
+ minWidth: 120,
158
+ sortable: true,
159
+ sortKey: "type",
160
+ filter: {
161
+ type: "select",
162
+ icon: "fa-list-check",
163
+ operators: ["is", "is_not"],
164
+ options: TYPE_FILTER_OPTS,
165
+ },
166
+ cell: row => <span className="text-sm text-foreground/90">{TYPE_LABEL[row.type]}</span>,
167
+ },
168
+ {
169
+ key: "difficulty",
170
+ label: "Difficulty",
171
+ width: 110,
172
+ minWidth: 96,
173
+ sortable: true,
174
+ sortKey: "difficulty",
175
+ filter: {
176
+ type: "select",
177
+ icon: "fa-signal",
178
+ operators: ["is", "is_not"],
179
+ options: DIFFICULTY_FILTER_OPTS,
180
+ },
181
+ cell: row => (
182
+ <span className="text-sm text-foreground/90">{DIFFICULTY_LABEL[row.difficulty]}</span>
183
+ ),
184
+ },
185
+ {
186
+ key: "status",
187
+ label: "Status",
188
+ width: 120,
189
+ minWidth: 100,
190
+ sortable: true,
191
+ sortKey: "status",
192
+ filter: {
193
+ type: "select",
194
+ icon: "fa-circle-dot",
195
+ operators: ["is", "is_not"],
196
+ options: STATUS_FILTER_OPTS,
197
+ },
198
+ cell: row => (
199
+ <ListHubStatusBadge
200
+ label={QUESTION_BANK_STATUS_LABEL[row.status]}
201
+ tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[row.status]}
202
+ icon={QUESTION_BANK_STATUS_ICON[row.status]}
203
+ />
204
+ ),
205
+ },
206
+ {
207
+ key: "updatedAt",
208
+ label: "Updated",
209
+ width: 120,
210
+ minWidth: 100,
211
+ sortable: true,
212
+ sortKey: "updatedAt",
213
+ filter: { type: "date", icon: "fa-calendar-days", operators: ["is", "is_not"] },
214
+ cell: row => (
215
+ <span className="text-sm tabular-nums text-foreground/90 whitespace-nowrap">{row.updatedAt}</span>
216
+ ),
217
+ },
218
+ {
219
+ key: "author",
220
+ label: "Author",
221
+ width: 140,
222
+ minWidth: 120,
223
+ sortable: true,
224
+ sortKey: "author",
225
+ filter: {
226
+ type: "text",
227
+ icon: "fa-user",
228
+ operators: ["contains", "not_contains"],
229
+ },
230
+ cell: row => <span className="text-sm text-foreground/90">{row.author}</span>,
231
+ },
232
+ {
233
+ key: "actions",
234
+ label: "",
235
+ width: 48,
236
+ minWidth: 48,
237
+ defaultPin: "right",
238
+ lockPin: true,
239
+ cell: row => (
240
+ <div className="flex items-center justify-center">
241
+ <DropdownMenu>
242
+ <DropdownMenuTrigger asChild>
243
+ <Button size="icon-sm" variant="ghost" aria-label={`Actions for question ${row.id}`}>
244
+ <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
245
+ </Button>
246
+ </DropdownMenuTrigger>
247
+ <DropdownMenuContent align="end" className="w-40">
248
+ <DropdownMenuItem disabled>
249
+ <i className="fa-light fa-eye" aria-hidden="true" />
250
+ Preview
251
+ </DropdownMenuItem>
252
+ <DropdownMenuItem disabled>
253
+ <i className="fa-light fa-pen" aria-hidden="true" />
254
+ Edit
255
+ </DropdownMenuItem>
256
+ </DropdownMenuContent>
257
+ </DropdownMenu>
258
+ </div>
259
+ ),
260
+ },
261
+ ]
262
+
263
+ return cols
264
+ }
265
+
266
+
267
+ function aggregateByStatus(rows: QuestionBankItem[]) {
268
+ const c = { published: 0, draft: 0, in_review: 0 }
269
+ for (const r of rows) c[r.status]++
270
+ return [
271
+ { name: QUESTION_BANK_STATUS_LABEL.published, value: c.published, key: "published" },
272
+ { name: QUESTION_BANK_STATUS_LABEL.draft, value: c.draft, key: "draft" },
273
+ { name: QUESTION_BANK_STATUS_LABEL.in_review, value: c.in_review, key: "in_review" },
274
+ ]
275
+ }
276
+
277
+ function aggregateByTopic(rows: QuestionBankItem[]) {
278
+ const map = new Map<string, number>()
279
+ for (const r of rows) map.set(r.topic, (map.get(r.topic) ?? 0) + 1)
280
+ return [...map.entries()]
281
+ .map(([name, value]) => ({ name: name.length > 20 ? `${name.slice(0, 18)}…` : name, value }))
282
+ .sort((a, b) => b.value - a.value)
283
+ .slice(0, 8)
284
+ }
285
+
286
+ function QuestionsByStatusChart({ rows }: { rows: QuestionBankItem[] }) {
287
+ const data = React.useMemo(() => aggregateByStatus(rows), [rows])
288
+ if (rows.length === 0) {
289
+ return (
290
+ <div className="flex h-[200px] items-center justify-center text-sm text-muted-foreground" role="status">
291
+ No questions in this view.
292
+ </div>
293
+ )
294
+ }
295
+ const summary = `Status breakdown: ${data.map(d => `${d.name} ${d.value}`).join(", ")}. Total ${rows.length}.`
296
+ return (
297
+ <ChartFigure label="Questions by status" summary={summary} dataLength={data.length}>
298
+ {(activeIndex) => (
299
+ <>
300
+ <ChartContainer config={BAR_CFG} className="h-[220px] w-full">
301
+ <BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
302
+ <CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
303
+ <XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
304
+ <YAxis allowDecimals={false} width={32} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
305
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
306
+ <Bar
307
+ dataKey="value"
308
+ fill="var(--color-chart-2)"
309
+ radius={[4, 4, 0, 0]}
310
+ maxBarSize={40}
311
+ activeBar={CHART_KBD_ACTIVE_BAR}
312
+ activeIndex={activeIndex ?? undefined}
313
+ >
314
+ {data.map((_, i) => (
315
+ <Cell key={i} fill="var(--color-chart-2)" />
316
+ ))}
317
+ </Bar>
318
+ </BarChart>
319
+ </ChartContainer>
320
+ <ChartDataTable
321
+ caption="Questions by status"
322
+ headers={["Status", "Count"]}
323
+ rows={data.map(d => [d.name, d.value])}
324
+ />
325
+ </>
326
+ )}
327
+ </ChartFigure>
328
+ )
329
+ }
330
+
331
+ function QuestionsByTopicChart({ rows }: { rows: QuestionBankItem[] }) {
332
+ const data = React.useMemo(() => aggregateByTopic(rows), [rows])
333
+ if (rows.length === 0) {
334
+ return (
335
+ <div className="flex h-[200px] items-center justify-center text-sm text-muted-foreground" role="status">
336
+ No questions in this view.
337
+ </div>
338
+ )
339
+ }
340
+ const summary = `${data.length} topics shown. Total ${rows.length} questions.`
341
+ return (
342
+ <ChartFigure label="Questions by topic" summary={summary} dataLength={data.length}>
343
+ {(activeIndex) => (
344
+ <>
345
+ <ChartContainer config={BAR_CFG} className="h-[220px] w-full">
346
+ <BarChart data={data} layout="vertical" margin={{ top: 8, right: 8, left: 4, bottom: 0 }}>
347
+ <CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
348
+ <XAxis type="number" allowDecimals={false} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
349
+ <YAxis type="category" dataKey="name" width={100} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
350
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
351
+ <Bar
352
+ dataKey="value"
353
+ fill="var(--color-chart-4)"
354
+ radius={[0, 4, 4, 0]}
355
+ maxBarSize={22}
356
+ activeBar={CHART_KBD_ACTIVE_BAR}
357
+ activeIndex={activeIndex ?? undefined}
358
+ >
359
+ {data.map((_, i) => (
360
+ <Cell key={i} fill="var(--color-chart-4)" />
361
+ ))}
362
+ </Bar>
363
+ </BarChart>
364
+ </ChartContainer>
365
+ <ChartDataTable
366
+ caption="Questions by topic"
367
+ headers={["Topic", "Count"]}
368
+ rows={data.map(d => [d.name, d.value])}
369
+ />
370
+ </>
371
+ )}
372
+ </ChartFigure>
373
+ )
374
+ }
375
+
376
+ function QuestionBankDashboardSimple({ rows }: { rows: QuestionBankItem[] }) {
377
+ const kpi = React.useMemo(
378
+ () => ({
379
+ metrics: questionBankKpiMetrics(rows),
380
+ insight: questionBankKpiInsight(rows),
381
+ }),
382
+ [rows],
383
+ )
384
+
385
+ return (
386
+ <div className="flex min-h-0 flex-1 flex-col gap-4 pb-6">
387
+ <KeyMetrics
388
+ variant="flat"
389
+ metrics={kpi.metrics}
390
+ insight={kpi.insight}
391
+ showHeader={false}
392
+ metricsSingleRow
393
+ />
394
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
395
+ <ChartCard variant="normal" title="By status" description="Filtered question set">
396
+ <QuestionsByStatusChart rows={rows} />
397
+ </ChartCard>
398
+ <ChartCard variant="normal" title="By topic" description="Up to eight topics">
399
+ <QuestionsByTopicChart rows={rows} />
400
+ </ChartCard>
401
+ </div>
402
+ </div>
403
+ )
404
+ }
405
+
406
+ export type QuestionBankTableHandle = OpenTablePropertiesHandle
407
+
408
+ export const QuestionBankTable = React.forwardRef<
409
+ QuestionBankTableHandle,
410
+ { items: QuestionBankItem[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
411
+ >(function QuestionBankTable({ items, view = "table", onViewChange }, ref) {
412
+ const columns = React.useMemo(() => buildQuestionBankColumns(items), [items])
413
+ const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
414
+ const fieldDefinitionsForDrawer = React.useMemo(
415
+ () =>
416
+ columns
417
+ .filter(c => c.key !== "select" && c.key !== "actions")
418
+ .map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
419
+ [columns],
420
+ )
421
+
422
+ const resolveColumnLabel = React.useCallback(
423
+ (key: string) => columns.find(c => c.key === key)?.label ?? key,
424
+ [columns],
425
+ )
426
+
427
+ const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
428
+ const patchDisplay = React.useCallback((patch: Partial<DataListDisplayOptions>) => {
429
+ setDisplayOptions(prev => ({ ...prev, ...patch }))
430
+ }, [])
431
+
432
+ const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
433
+ const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
434
+ setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
435
+ }, [])
436
+ const removeConditionalRule = React.useCallback((id: string) => {
437
+ setConditionalRules(prev => prev.filter(r => r.id !== id))
438
+ }, [])
439
+ const updateConditionalRule = React.useCallback((id: string, patch: Partial<ConditionalRule>) => {
440
+ setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
441
+ }, [])
442
+
443
+ const tableState = useTableState(items, columns, { key: "updatedAt", dir: "desc" })
444
+
445
+ const renderFilterOptionValue = React.useCallback(
446
+ (fieldKey: string, value: string): React.ReactNode => {
447
+ if (fieldKey === "status") {
448
+ if (value in QUESTION_BANK_STATUS_LABEL) {
449
+ const status = value as keyof typeof QUESTION_BANK_STATUS_LABEL
450
+ return (
451
+ <ListHubStatusBadge
452
+ label={QUESTION_BANK_STATUS_LABEL[status]}
453
+ tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[status]}
454
+ icon={QUESTION_BANK_STATUS_ICON[status]}
455
+ />
456
+ )
457
+ }
458
+ return <span className="text-foreground">{value}</span>
459
+ }
460
+ const col = columns.find(c => c.key === fieldKey)
461
+ const opt = col?.filter?.options?.find(o => o.value === value)
462
+ return <span className="text-foreground">{opt?.label ?? value}</span>
463
+ },
464
+ [columns],
465
+ )
466
+
467
+ React.useImperativeHandle(ref, () => ({
468
+ openPropertiesDrawer: () => {
469
+ tableState.setSheetOpen(true)
470
+ },
471
+ }), [tableState])
472
+
473
+ const questionBankBoardGroupKey = QUESTION_BANK_BOARD_GROUP_OPTIONS.some(
474
+ o => o.key === displayOptions.boardGroupByColumnKey,
475
+ )
476
+ ? displayOptions.boardGroupByColumnKey
477
+ : "status"
478
+
479
+ const drawerToolbarProps = {
480
+ totalRows: items.length,
481
+ filterFields,
482
+ fieldDefinitions: fieldDefinitionsForDrawer,
483
+ resolveColumnLabel,
484
+ displayOptions,
485
+ onDisplayOptionsChange: patchDisplay,
486
+ conditionalRules,
487
+ onAddConditionalRule: addConditionalRule,
488
+ onRemoveConditionalRule: removeConditionalRule,
489
+ onUpdateConditionalRule: updateConditionalRule,
490
+ currentView: view,
491
+ onViewChange,
492
+ lifecycleTabLabel: "Question bank",
493
+ boardGroupByColumnOptions: [...QUESTION_BANK_BOARD_GROUP_OPTIONS],
494
+ renderFilterOptionValue,
495
+ }
496
+
497
+ const tableProps = {
498
+ data: items,
499
+ columns,
500
+ getRowId: (row: QuestionBankItem) => row.id,
501
+ getRowSelectionLabel: (row: QuestionBankItem) => row.stem,
502
+ selectable: true,
503
+ searchable: displayOptions.showToolbarSearch,
504
+ showColumnHeaders: displayOptions.showColumnLabels,
505
+ groupable: true,
506
+ defaultSort: { key: "updatedAt", dir: "desc" as const },
507
+ emptyState: <p className="text-sm text-muted-foreground">No questions in the bank.</p>,
508
+ conditionalRules,
509
+ state: tableState,
510
+ renderFilterOptionValue,
511
+ toolbarSlot: (s: ReturnType<typeof useTableState<QuestionBankItem>>) => (
512
+ <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />
513
+ ),
514
+ bulkActionsSlot: (selected: Set<string | number>) => {
515
+ const n = selected.size
516
+ if (n === 0) return null
517
+ return (
518
+ <>
519
+ <span className="sr-only">{n} selected</span>
520
+ <Tip label="Export selection (demo)">
521
+ <Button size="sm" variant="outline" type="button">
522
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
523
+ Export
524
+ </Button>
525
+ </Tip>
526
+ </>
527
+ )
528
+ },
529
+ }
530
+
531
+ if (view === "table") {
532
+ return (
533
+ <div className="pb-6">
534
+ <DataTable<QuestionBankItem> {...tableProps} />
535
+ </div>
536
+ )
537
+ }
538
+
539
+ const sharedToolbar = (
540
+ <DataTableToolbar
541
+ state={tableState}
542
+ columns={columns}
543
+ searchable={displayOptions.showToolbarSearch}
544
+ searchAriaLabel="Search questions"
545
+ renderFilterOptionValue={renderFilterOptionValue}
546
+ toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
547
+ />
548
+ )
549
+
550
+ if (view === "list") {
551
+ return (
552
+ <div className="flex min-h-0 flex-1 flex-col">
553
+ {sharedToolbar}
554
+ <QuestionBankListView rows={tableState.rows as QuestionBankItem[]} />
555
+ </div>
556
+ )
557
+ }
558
+
559
+ if (view === "board") {
560
+ return (
561
+ <div className="flex min-h-0 flex-1 flex-col">
562
+ {sharedToolbar}
563
+ <QuestionBankBoardView
564
+ rows={tableState.rows as QuestionBankItem[]}
565
+ groupByColumnKey={questionBankBoardGroupKey}
566
+ />
567
+ </div>
568
+ )
569
+ }
570
+
571
+ return (
572
+ <div className="flex min-h-0 flex-1 flex-col">
573
+ <DataTableToolbar
574
+ state={tableState}
575
+ columns={columns}
576
+ searchable={displayOptions.showToolbarSearch}
577
+ searchAriaLabel="Search questions"
578
+ renderFilterOptionValue={renderFilterOptionValue}
579
+ toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
580
+ />
581
+ <QuestionBankDashboardSimple rows={tableState.rows as QuestionBankItem[]} />
582
+ </div>
583
+ )
584
+ })
585
+
586
+ QuestionBankTable.displayName = "QuestionBankTable"
@@ -0,0 +1,47 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Rotations hub — main canvas when no rotation detail is selected.
5
+ * Pairs with SecondaryPanel (nested sidebar); CTA reopens the panel if closed.
6
+ */
7
+
8
+ import { Button } from "@/components/ui/button"
9
+ import { useSecondaryPanel } from "@/components/secondary-panel"
10
+
11
+ export function RotationsEmptyState() {
12
+ const { openPanel } = useSecondaryPanel()
13
+
14
+ return (
15
+ <section
16
+ aria-labelledby="rotations-empty-title"
17
+ className="flex flex-1 flex-col items-center justify-center rounded-xl border border-dashed border-border/80 bg-muted/25 px-6 py-12 text-center min-h-[min(420px,calc(100svh-var(--header-height)-6rem))]"
18
+ >
19
+ <div className="mb-6 w-full max-w-[min(100%,280px)] shrink-0">
20
+ <img
21
+ src="/Illustration/Rotation.svg"
22
+ alt=""
23
+ width={622}
24
+ height={559}
25
+ decoding="async"
26
+ className="h-auto w-full select-none"
27
+ />
28
+ </div>
29
+ <h2
30
+ id="rotations-empty-title"
31
+ className="font-heading text-xl font-semibold tracking-tight text-foreground sm:text-2xl"
32
+ >
33
+ Select a rotation
34
+ </h2>
35
+ <p className="mt-2 max-w-md text-sm leading-relaxed text-muted-foreground">
36
+ Use the rotations panel next to the sidebar to browse cycles, open a rotation for
37
+ details, or review schedules and assigned students.
38
+ </p>
39
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
40
+ <Button type="button" size="lg" onClick={() => openPanel("rotations")}>
41
+ <i className="fa-light fa-sidebar text-[15px]" aria-hidden="true" />
42
+ Open rotations panel
43
+ </Button>
44
+ </div>
45
+ </section>
46
+ )
47
+ }
@@ -0,0 +1,8 @@
1
+ "use client"
2
+
3
+ import { useAutoPanel } from "@/components/secondary-panel"
4
+
5
+ export function RotationsPanelActivator() {
6
+ useAutoPanel("rotations")
7
+ return null
8
+ }