@exxatdesignux/ui 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/bin/init.mjs +29 -0
  2. package/package.json +7 -2
  3. package/template/.nvmrc +1 -0
  4. package/template/.prettierignore +7 -0
  5. package/template/.prettierrc +11 -0
  6. package/template/AGENTS.md +485 -0
  7. package/template/Logo/Exxat_Prism.svg +39 -0
  8. package/template/Logo/Exxat_one.svg +36 -0
  9. package/template/README.md +58 -0
  10. package/template/app/(app)/compliance/page.tsx +10 -0
  11. package/template/app/(app)/dashboard/loading.tsx +18 -0
  12. package/template/app/(app)/dashboard/page.tsx +36 -0
  13. package/template/app/(app)/data-list/[id]/page.tsx +28 -0
  14. package/template/app/(app)/data-list/new/page.tsx +31 -0
  15. package/template/app/(app)/data-list/page.tsx +10 -0
  16. package/template/app/(app)/error.tsx +43 -0
  17. package/template/app/(app)/help/page.tsx +34 -0
  18. package/template/app/(app)/layout.tsx +54 -0
  19. package/template/app/(app)/loading.tsx +18 -0
  20. package/template/app/(app)/question-bank/page.tsx +10 -0
  21. package/template/app/(app)/rotations/page.tsx +15 -0
  22. package/template/app/(app)/settings/page.tsx +17 -0
  23. package/template/app/(app)/sites/all/page.tsx +13 -0
  24. package/template/app/(app)/team/page.tsx +10 -0
  25. package/template/app/favicon.ico +0 -0
  26. package/template/app/globals.css +1811 -0
  27. package/template/app/layout.tsx +95 -0
  28. package/template/app/page.tsx +9 -0
  29. package/template/components/.gitkeep +0 -0
  30. package/template/components/app-sidebar-dynamic.tsx +15 -0
  31. package/template/components/app-sidebar.tsx +901 -0
  32. package/template/components/ask-leo-composer.tsx +216 -0
  33. package/template/components/ask-leo-sidebar.tsx +509 -0
  34. package/template/components/chart-area-interactive.tsx +293 -0
  35. package/template/components/charts-overview.tsx +2321 -0
  36. package/template/components/command-menu-01.tsx +133 -0
  37. package/template/components/command-menu-02.tsx +386 -0
  38. package/template/components/command-menu.tsx +182 -0
  39. package/template/components/compliance-board-view.tsx +134 -0
  40. package/template/components/compliance-client.tsx +92 -0
  41. package/template/components/compliance-list-view.tsx +59 -0
  42. package/template/components/compliance-page-header.tsx +89 -0
  43. package/template/components/compliance-table.tsx +525 -0
  44. package/template/components/dashboard-onboarding-gallery.tsx +13 -0
  45. package/template/components/dashboard-onboarding.tsx +21 -0
  46. package/template/components/dashboard-promo-banner.tsx +67 -0
  47. package/template/components/dashboard-quota-progress-card.tsx +369 -0
  48. package/template/components/dashboard-report-charts.tsx +69 -0
  49. package/template/components/dashboard-section-heading.tsx +68 -0
  50. package/template/components/dashboard-tabs.tsx +598 -0
  51. package/template/components/data-list-client.tsx +239 -0
  52. package/template/components/data-list-table-cells.test.tsx +22 -0
  53. package/template/components/data-list-table-cells.tsx +173 -0
  54. package/template/components/data-list-table.tsx +879 -0
  55. package/template/components/data-table/filter-date-calendar.tsx +38 -0
  56. package/template/components/data-table/filter-text-value-input.tsx +77 -0
  57. package/template/components/data-table/index.tsx +1612 -0
  58. package/template/components/data-table/pagination.tsx +256 -0
  59. package/template/components/data-table/types.ts +91 -0
  60. package/template/components/data-table/use-table-state.ts +566 -0
  61. package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
  62. package/template/components/data-view-dashboard-charts-team.tsx +968 -0
  63. package/template/components/data-view-dashboard-charts.tsx +1668 -0
  64. package/template/components/data-views/board-card-primitives.tsx +93 -0
  65. package/template/components/data-views/index.ts +41 -0
  66. package/template/components/data-views/list-page-board-card.tsx +192 -0
  67. package/template/components/data-views/list-page-board-template.tsx +122 -0
  68. package/template/components/data-views/placement-board-card.tsx +262 -0
  69. package/template/components/export-drawer.tsx +375 -0
  70. package/template/components/exxat-product-logo.tsx +453 -0
  71. package/template/components/form-layout-01.tsx +131 -0
  72. package/template/components/getting-started.tsx +625 -0
  73. package/template/components/key-metrics.tsx +920 -0
  74. package/template/components/leo-insight-indicator.tsx +364 -0
  75. package/template/components/leo-typing-dots.tsx +121 -0
  76. package/template/components/list-hub-status-badge.tsx +51 -0
  77. package/template/components/list-page-dashboard-charts.tsx +18 -0
  78. package/template/components/nav-documents.tsx +89 -0
  79. package/template/components/nav-main.tsx +58 -0
  80. package/template/components/nav-secondary.tsx +64 -0
  81. package/template/components/nav-user.tsx +190 -0
  82. package/template/components/new-placement-back-btn.tsx +28 -0
  83. package/template/components/new-placement-form.tsx +1066 -0
  84. package/template/components/onboarding/index.ts +4 -0
  85. package/template/components/onboarding/onboarding-01.tsx +7 -0
  86. package/template/components/onboarding/onboarding-02.tsx +7 -0
  87. package/template/components/onboarding/onboarding-03.tsx +7 -0
  88. package/template/components/onboarding/onboarding-04.tsx +7 -0
  89. package/template/components/page-header.tsx +57 -0
  90. package/template/components/placement-detail.tsx +438 -0
  91. package/template/components/placements-board-view.tsx +404 -0
  92. package/template/components/placements-list-view.tsx +285 -0
  93. package/template/components/placements-page-header.tsx +160 -0
  94. package/template/components/placements-table-columns.tsx +639 -0
  95. package/template/components/product-switcher.tsx +116 -0
  96. package/template/components/question-bank-board-view.tsx +205 -0
  97. package/template/components/question-bank-client.tsx +77 -0
  98. package/template/components/question-bank-list-view.tsx +59 -0
  99. package/template/components/question-bank-page-header.tsx +89 -0
  100. package/template/components/question-bank-table.tsx +586 -0
  101. package/template/components/rotations-empty-state.tsx +47 -0
  102. package/template/components/rotations-panel-activator.tsx +8 -0
  103. package/template/components/secondary-nav.tsx +394 -0
  104. package/template/components/secondary-panel.tsx +239 -0
  105. package/template/components/section-cards.tsx +106 -0
  106. package/template/components/settings-appearance-card.tsx +424 -0
  107. package/template/components/settings-client.tsx +537 -0
  108. package/template/components/settings-form-row.tsx +42 -0
  109. package/template/components/sidebar-auto-collapse.tsx +23 -0
  110. package/template/components/sidebar-auto-open.tsx +18 -0
  111. package/template/components/sidebar-shell.tsx +37 -0
  112. package/template/components/site-header.tsx +93 -0
  113. package/template/components/sites-all-client.tsx +154 -0
  114. package/template/components/sites-board-view.tsx +67 -0
  115. package/template/components/sites-list-view.tsx +47 -0
  116. package/template/components/sites-table.tsx +312 -0
  117. package/template/components/system-banner-slot.tsx +66 -0
  118. package/template/components/table-properties/column-row.tsx +90 -0
  119. package/template/components/table-properties/draggable-list.ts +49 -0
  120. package/template/components/table-properties/drawer-button.tsx +231 -0
  121. package/template/components/table-properties/drawer.tsx +1102 -0
  122. package/template/components/table-properties/filter-card.tsx +251 -0
  123. package/template/components/table-properties/index.ts +22 -0
  124. package/template/components/table-properties/sort-card.tsx +59 -0
  125. package/template/components/table-properties/types.ts +124 -0
  126. package/template/components/task-list-panel.tsx +98 -0
  127. package/template/components/task-priority-badge.tsx +28 -0
  128. package/template/components/team-board-view.tsx +114 -0
  129. package/template/components/team-client.tsx +93 -0
  130. package/template/components/team-list-view.tsx +62 -0
  131. package/template/components/team-page-header.tsx +92 -0
  132. package/template/components/team-table.tsx +525 -0
  133. package/template/components/templates/list-page.tsx +576 -0
  134. package/template/components/templates/primary-page-template.tsx +56 -0
  135. package/template/components/theme-color-sync.tsx +32 -0
  136. package/template/components/theme-provider.tsx +71 -0
  137. package/template/components/tinted-icon-disc.tsx +53 -0
  138. package/template/components/ui/ai-thinking-surface.tsx +121 -0
  139. package/template/components/ui/avatar.tsx +1 -0
  140. package/template/components/ui/badge.tsx +1 -0
  141. package/template/components/ui/banner.tsx +1 -0
  142. package/template/components/ui/breadcrumb.tsx +1 -0
  143. package/template/components/ui/button.tsx +1 -0
  144. package/template/components/ui/calendar.tsx +1 -0
  145. package/template/components/ui/card.tsx +1 -0
  146. package/template/components/ui/chart.tsx +1 -0
  147. package/template/components/ui/checkbox.tsx +1 -0
  148. package/template/components/ui/coach-mark.tsx +1 -0
  149. package/template/components/ui/collapsible.tsx +1 -0
  150. package/template/components/ui/command.tsx +1 -0
  151. package/template/components/ui/date-picker-field.tsx +1 -0
  152. package/template/components/ui/dialog.tsx +1 -0
  153. package/template/components/ui/dot-pattern.tsx +159 -0
  154. package/template/components/ui/drag-handle-grip.tsx +1 -0
  155. package/template/components/ui/drawer.tsx +1 -0
  156. package/template/components/ui/dropdown-menu.tsx +1 -0
  157. package/template/components/ui/field.tsx +1 -0
  158. package/template/components/ui/form.tsx +1 -0
  159. package/template/components/ui/input-group.tsx +1 -0
  160. package/template/components/ui/input-mask.tsx +1 -0
  161. package/template/components/ui/input.tsx +1 -0
  162. package/template/components/ui/kbd.tsx +1 -0
  163. package/template/components/ui/label.tsx +1 -0
  164. package/template/components/ui/leo-icon.tsx +726 -0
  165. package/template/components/ui/payment-card-fields.tsx +1 -0
  166. package/template/components/ui/popover.tsx +1 -0
  167. package/template/components/ui/radio-group.tsx +1 -0
  168. package/template/components/ui/select.tsx +1 -0
  169. package/template/components/ui/selection-tile-grid.tsx +1 -0
  170. package/template/components/ui/separator.tsx +1 -0
  171. package/template/components/ui/sheet.tsx +1 -0
  172. package/template/components/ui/sidebar.tsx +1 -0
  173. package/template/components/ui/skeleton.tsx +1 -0
  174. package/template/components/ui/sonner.tsx +1 -0
  175. package/template/components/ui/status-badge.tsx +1 -0
  176. package/template/components/ui/table.tsx +1 -0
  177. package/template/components/ui/tabs.tsx +1 -0
  178. package/template/components/ui/textarea.tsx +1 -0
  179. package/template/components/ui/tip.tsx +1 -0
  180. package/template/components/ui/toggle-group.tsx +1 -0
  181. package/template/components/ui/toggle-switch.tsx +1 -0
  182. package/template/components/ui/toggle.tsx +1 -0
  183. package/template/components/ui/tooltip.tsx +1 -0
  184. package/template/components/ui/view-segmented-control.tsx +1 -0
  185. package/template/components.json +27 -0
  186. package/template/contexts/chart-variant-context.tsx +35 -0
  187. package/template/contexts/command-menu-context.tsx +28 -0
  188. package/template/contexts/dashboard-view-context.tsx +35 -0
  189. package/template/contexts/product-context.tsx +38 -0
  190. package/template/contexts/system-banner-context.tsx +127 -0
  191. package/template/docs/command-menu-pattern.md +45 -0
  192. package/template/docs/data-views-pattern.md +160 -0
  193. package/template/ecosystem.config.cjs +20 -0
  194. package/template/eslint.config.mjs +18 -0
  195. package/template/fontawesome-subset.manifest.json +190 -0
  196. package/template/hooks/.gitkeep +0 -0
  197. package/template/hooks/use-app-theme.ts +1 -0
  198. package/template/hooks/use-coach-mark.ts +1 -0
  199. package/template/hooks/use-mobile.ts +1 -0
  200. package/template/hooks/use-mod-key-label.ts +1 -0
  201. package/template/lib/.gitkeep +0 -0
  202. package/template/lib/ask-leo-route-context.ts +133 -0
  203. package/template/lib/chart-keyboard-selection.test.ts +20 -0
  204. package/template/lib/chart-keyboard-selection.ts +17 -0
  205. package/template/lib/chart-line-dash.ts +16 -0
  206. package/template/lib/coach-mark-registry.ts +68 -0
  207. package/template/lib/command-menu-config.ts +127 -0
  208. package/template/lib/command-menu-search-data.ts +44 -0
  209. package/template/lib/conditional-rule-match.ts +32 -0
  210. package/template/lib/dashboard-customize-coach-mark.ts +18 -0
  211. package/template/lib/dashboard-layout-merge.ts +63 -0
  212. package/template/lib/data-list-display-options.ts +35 -0
  213. package/template/lib/data-list-persistence.ts +280 -0
  214. package/template/lib/data-list-view-surface.ts +58 -0
  215. package/template/lib/data-list-view.ts +29 -0
  216. package/template/lib/data-view-dashboard-storage.ts +101 -0
  217. package/template/lib/date-filter.ts +8 -0
  218. package/template/lib/dev-log.test.ts +28 -0
  219. package/template/lib/dev-log.ts +8 -0
  220. package/template/lib/editable-target.ts +10 -0
  221. package/template/lib/floating-sheet-panel.ts +72 -0
  222. package/template/lib/initials-from-name.ts +7 -0
  223. package/template/lib/list-page-table-properties.ts +52 -0
  224. package/template/lib/list-status-badges.ts +168 -0
  225. package/template/lib/logo-dev.ts +12 -0
  226. package/template/lib/mock/compliance-kpi.ts +61 -0
  227. package/template/lib/mock/compliance.ts +146 -0
  228. package/template/lib/mock/dashboard.ts +105 -0
  229. package/template/lib/mock/navigation.tsx +231 -0
  230. package/template/lib/mock/placements-kpi.ts +134 -0
  231. package/template/lib/mock/placements.ts +183 -0
  232. package/template/lib/mock/question-bank-kpi.ts +61 -0
  233. package/template/lib/mock/question-bank.ts +142 -0
  234. package/template/lib/mock/sites-directory.ts +16 -0
  235. package/template/lib/mock/sites-kpi.ts +25 -0
  236. package/template/lib/mock/team-kpi.ts +60 -0
  237. package/template/lib/mock/team.ts +118 -0
  238. package/template/lib/motion-ui.ts +17 -0
  239. package/template/lib/placement-board-card-layout.ts +79 -0
  240. package/template/lib/placement-lifecycle.ts +5 -0
  241. package/template/lib/row-height.ts +10 -0
  242. package/template/lib/stock-portrait.ts +11 -0
  243. package/template/lib/utils.test.ts +13 -0
  244. package/template/lib/utils.ts +1 -0
  245. package/template/next.config.mjs +15 -0
  246. package/template/package.json +83 -0
  247. package/template/postcss.config.mjs +8 -0
  248. package/template/public/.gitkeep +0 -0
  249. package/template/public/Illustration/Rotation.svg +74 -0
  250. package/template/public/avatars/user.svg +11 -0
  251. package/template/public/favicon/favicon.ico +0 -0
  252. package/template/public/favicon.ico +0 -0
  253. package/template/public/logos/exxat-one.svg +36 -0
  254. package/template/public/logos/exxat-prism.svg +39 -0
  255. package/template/public/mock-schools/emory.svg +4 -0
  256. package/template/public/mock-schools/rush.svg +4 -0
  257. package/template/scripts/fontawesome-subset-audit.mjs +190 -0
  258. package/template/scripts/pm2-startup-macos.sh +13 -0
  259. package/template/skills-lock.json +10 -0
  260. package/template/stores/app-store.ts +33 -0
  261. package/template/tests/setup.ts +1 -0
  262. package/template/tsconfig.json +35 -0
  263. package/template/types/react-payment-inputs.d.ts +19 -0
  264. package/template/vitest.config.ts +18 -0
@@ -0,0 +1,369 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Student score bands — linear bars (min–max scale, class avg marker, student score)
5
+ * and radial summary. ChartFigure wiring for the radial lives in charts-overview.tsx.
6
+ */
7
+
8
+ import * as React from "react"
9
+ import { PolarAngleAxis, RadialBar, RadialBarChart } from "recharts"
10
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
11
+ import {
12
+ ChartContainer,
13
+ ChartTooltip,
14
+ chartTooltipKeyboardSyncProps,
15
+ ChartTooltipContent,
16
+ type ChartConfig,
17
+ } from "@/components/ui/chart"
18
+ import { cn } from "@/lib/utils"
19
+ import { isEditableTarget } from "@/lib/editable-target"
20
+ import {
21
+ DASHBOARD_STUDENT_SCORES,
22
+ formatBandScore,
23
+ scoreToTrackPercent,
24
+ type DashboardStudentScoresData,
25
+ type StudentScoreMetric,
26
+ type StudentScoreRadial,
27
+ } from "@/lib/mock/dashboard"
28
+
29
+ const scoreRadialCfg: ChartConfig = {
30
+ score: { label: "Student score", color: "var(--brand-color)" },
31
+ }
32
+
33
+ /** Same structure as ChartDataTable — local to avoid importing charts-overview (cycle). */
34
+ function SrOnlyMetricTable({
35
+ caption,
36
+ headers,
37
+ rows,
38
+ }: {
39
+ caption: string
40
+ headers: string[]
41
+ rows: (string | number)[][]
42
+ }) {
43
+ return (
44
+ <table className="sr-only">
45
+ <caption>{caption}</caption>
46
+ <thead>
47
+ <tr>{headers.map((h) => <th key={h} scope="col">{h}</th>)}</tr>
48
+ </thead>
49
+ <tbody>
50
+ {rows.map((row, i) => (
51
+ <tr key={i}>{row.map((cell, j) => <td key={j}>{cell}</td>)}</tr>
52
+ ))}
53
+ </tbody>
54
+ </table>
55
+ )
56
+ }
57
+
58
+ const scaleEndsClass =
59
+ "flex justify-between gap-2 px-0.5 text-xs tabular-nums leading-none text-muted-foreground"
60
+
61
+ const linearProgressFocusClass =
62
+ "rounded-md p-1.5 -m-1.5 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
63
+
64
+ function StudentScoreProgressRow({
65
+ label,
66
+ scaleMin,
67
+ scaleMax,
68
+ classAverage,
69
+ studentScore,
70
+ averageMarkerLabel = "Class avg",
71
+ }: {
72
+ label: string
73
+ scaleMin: number
74
+ scaleMax: number
75
+ classAverage: number
76
+ studentScore: number
77
+ averageMarkerLabel?: string
78
+ }) {
79
+ const fillPct = scoreToTrackPercent(studentScore, scaleMin, scaleMax)
80
+ const avgPct = scoreToTrackPercent(classAverage, scaleMin, scaleMax)
81
+ const minStr = formatBandScore(scaleMin)
82
+ const maxStr = formatBandScore(scaleMax)
83
+ const labelId = React.useId()
84
+ const kbdHintId = React.useId()
85
+ const valueText = `Score ${formatBandScore(studentScore)}. ${averageMarkerLabel} ${formatBandScore(classAverage)}. Scale ${minStr} through ${maxStr}.`
86
+
87
+ function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
88
+ if (e.key !== "Escape") return
89
+ if (isEditableTarget(e.target)) return
90
+ e.preventDefault()
91
+ e.stopPropagation()
92
+ e.currentTarget.blur()
93
+ }
94
+
95
+ /** Clicks on the track do not always move focus — align with ChartFigure pointer focus. */
96
+ function handlePointerDownCapture(e: React.PointerEvent<HTMLDivElement>) {
97
+ const root = e.currentTarget
98
+ const el = e.target as HTMLElement | null
99
+ if (
100
+ el?.closest?.(
101
+ "button, a, [role='tab'], [role='option'], input, select, textarea, [contenteditable='true']",
102
+ )
103
+ )
104
+ return
105
+ queueMicrotask(() => root.focus())
106
+ }
107
+
108
+ return (
109
+ <div>
110
+ <p id={labelId} className="text-xs font-medium text-foreground">
111
+ {label}
112
+ </p>
113
+ <div className="mt-2 flex items-start gap-3 sm:gap-4">
114
+ <div
115
+ tabIndex={0}
116
+ role="progressbar"
117
+ aria-labelledby={labelId}
118
+ aria-describedby={kbdHintId}
119
+ aria-valuemin={scaleMin}
120
+ aria-valuemax={scaleMax}
121
+ aria-valuenow={studentScore}
122
+ aria-valuetext={valueText}
123
+ onKeyDown={handleKeyDown}
124
+ onPointerDownCapture={handlePointerDownCapture}
125
+ className={cn("min-w-0 flex-1", linearProgressFocusClass)}
126
+ >
127
+ <span id={kbdHintId} className="sr-only">
128
+ Tab to focus this score bar. Press Escape to leave focus.
129
+ </span>
130
+ <div className={cn(scaleEndsClass, "mb-1")} aria-hidden="true">
131
+ <span>{minStr}</span>
132
+ <span>{maxStr}</span>
133
+ </div>
134
+ <div className="relative pb-7">
135
+ {/* High-contrast (data-contrast="high") & Windows forced-colors:
136
+ without these overrides the track, fill, and avg marker all
137
+ collapse to the same value in HC themes (see a11y bug).
138
+ - track: keep an outlined container so it's visible on the HC bg
139
+ - fill: use foreground color (full contrast) instead of tinted brand
140
+ - pill: invert with a visible border so label stays legible */}
141
+ {/* HC dark: track = transparent with a thin border (so card bg
142
+ shows through), fill = foreground (white on dark HC). Light HC:
143
+ same pattern — fill resolves to near-black on light. Never
144
+ invert: the FILL must be the high-contrast stroke, never the
145
+ track. */}
146
+ <div className="relative h-3 w-full overflow-visible rounded-full bg-muted hc:border hc:border-foreground hc:bg-transparent forced-colors:border forced-colors:border-[CanvasText] forced-colors:bg-[Canvas]">
147
+ <div
148
+ className="absolute inset-y-0 left-0 rounded-full bg-brand hc:bg-foreground forced-colors:bg-[Highlight]"
149
+ style={{ width: `${fillPct}%` }}
150
+ aria-hidden="true"
151
+ />
152
+ <div
153
+ className="pointer-events-none absolute inset-y-0 z-[1] w-0 -translate-x-1/2 border-l border-dashed border-muted-foreground/70 hc:border-foreground forced-colors:border-[CanvasText]"
154
+ style={{ left: `${avgPct}%` }}
155
+ aria-hidden="true"
156
+ />
157
+ </div>
158
+ <span
159
+ className={cn(
160
+ "pointer-events-none absolute top-[calc(0.75rem+0.375rem)] max-w-[5.5rem] -translate-x-1/2 rounded-md px-1.5 py-0.5 text-center",
161
+ "bg-foreground text-xs font-medium leading-tight text-background",
162
+ "hc:border hc:border-foreground hc:bg-background hc:text-foreground",
163
+ "forced-colors:border forced-colors:border-[CanvasText] forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]",
164
+ )}
165
+ style={{ left: `${avgPct}%` }}
166
+ aria-hidden="true"
167
+ >
168
+ {averageMarkerLabel}
169
+ </span>
170
+ </div>
171
+ <div className={cn(scaleEndsClass, "mt-1")} aria-hidden="true">
172
+ <span>{minStr}</span>
173
+ <span>{maxStr}</span>
174
+ </div>
175
+ </div>
176
+ <p
177
+ className="shrink-0 text-xl font-bold tabular-nums text-foreground sm:text-2xl"
178
+ aria-hidden="true"
179
+ >
180
+ {formatBandScore(studentScore)}
181
+ </p>
182
+ </div>
183
+ </div>
184
+ )
185
+ }
186
+
187
+ /** Recharts radial: ring = position of student score on scale; center shows raw score. */
188
+ export function QuotaRadialChartInner({
189
+ radial,
190
+ activeIndex,
191
+ }: {
192
+ radial: StudentScoreRadial
193
+ activeIndex: number | null
194
+ }) {
195
+ const fill = scoreToTrackPercent(radial.studentScore, radial.scaleMin, radial.scaleMax)
196
+ /* Fill + track reference CSS vars so HC mode can override them without
197
+ re-rendering the chart. Default: brand fill over muted track.
198
+ HC (`data-contrast="high"`): fill = foreground (full contrast), track =
199
+ transparent with a visible ring via strokeOpacity on the bg bar. */
200
+ const chartData = [{ name: "score", value: fill, fill: "var(--progress-fill, var(--brand-color))" }]
201
+
202
+ return (
203
+ <div
204
+ className="relative mx-auto w-full max-w-[220px] hc:[--progress-fill:var(--foreground)] hc:[--progress-track:transparent] hc:[--progress-track-stroke:var(--foreground)] forced-colors:[--progress-fill:Highlight] forced-colors:[--progress-track:Canvas] forced-colors:[--progress-track-stroke:CanvasText]"
205
+ >
206
+ <ChartContainer config={scoreRadialCfg} className="mx-auto aspect-square w-full max-h-[220px]">
207
+ <RadialBarChart
208
+ data={chartData}
209
+ innerRadius="58%"
210
+ outerRadius="92%"
211
+ startAngle={90}
212
+ endAngle={-270}
213
+ barSize={14}
214
+ >
215
+ <PolarAngleAxis type="number" domain={[0, 100]} tick={false} />
216
+ <ChartTooltip
217
+ key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props}
218
+ content={(
219
+ <ChartTooltipContent
220
+ hideLabel
221
+ formatter={() => (
222
+ <span className="tabular-nums">
223
+ Score {formatBandScore(radial.studentScore)} · {formatBandScore(radial.scaleMin)}–
224
+ {formatBandScore(radial.scaleMax)} · Class avg {formatBandScore(radial.classAverage)}
225
+ </span>
226
+ )}
227
+ />
228
+ )}
229
+ />
230
+ <RadialBar
231
+ dataKey="value"
232
+ cornerRadius={8}
233
+ background={{
234
+ fill: "var(--progress-track, var(--muted))",
235
+ stroke: "var(--progress-track-stroke, transparent)",
236
+ strokeWidth: 1,
237
+ }}
238
+ activeIndex={activeIndex ?? undefined}
239
+ />
240
+ </RadialBarChart>
241
+ </ChartContainer>
242
+ <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
243
+ <span className="text-2xl font-bold tabular-nums text-foreground">
244
+ {formatBandScore(radial.studentScore)}
245
+ </span>
246
+ <span className="mt-0.5 max-w-[10rem] text-center text-xs text-muted-foreground leading-snug">
247
+ {radial.caption}
248
+ </span>
249
+ </div>
250
+ </div>
251
+ )
252
+ }
253
+
254
+ export function QuotaRadialGaugeStatic({ radial }: { radial: StudentScoreRadial }) {
255
+ return (
256
+ <div className="flex flex-col items-center gap-3">
257
+ <QuotaRadialChartInner radial={radial} activeIndex={null} />
258
+ <p className="text-xs text-muted-foreground tabular-nums">
259
+ Class avg{" "}
260
+ <span className="font-medium text-foreground">{formatBandScore(radial.classAverage)}</span>
261
+ <span className="text-muted-foreground">
262
+ {" "}
263
+ · scale {formatBandScore(radial.scaleMin)}–{formatBandScore(radial.scaleMax)}
264
+ </span>
265
+ </p>
266
+ </div>
267
+ )
268
+ }
269
+
270
+ export function QuotaLinearProgressCardBody({
271
+ metric,
272
+ suiteContext,
273
+ }: {
274
+ metric: StudentScoreMetric
275
+ suiteContext: string
276
+ }) {
277
+ const summaryId = React.useId()
278
+ const { scaleMin, scaleMax, classAverage, studentScore } = metric
279
+
280
+ return (
281
+ <div
282
+ className="flex min-h-[120px] flex-1 flex-col justify-center"
283
+ role="region"
284
+ aria-describedby={summaryId}
285
+ >
286
+ <p id={summaryId} className="sr-only">
287
+ {metric.label}: student score {formatBandScore(studentScore)}. Class average {formatBandScore(classAverage)}. Scale
288
+ from {formatBandScore(scaleMin)} to {formatBandScore(scaleMax)}. {suiteContext}
289
+ </p>
290
+ <StudentScoreProgressRow
291
+ label={metric.label}
292
+ scaleMin={scaleMin}
293
+ scaleMax={scaleMax}
294
+ classAverage={classAverage}
295
+ studentScore={studentScore}
296
+ averageMarkerLabel={metric.averageMarkerLabel}
297
+ />
298
+ <SrOnlyMetricTable
299
+ caption={metric.label}
300
+ headers={["Assessment", "Student score", "Class average", "Scale min", "Scale max"]}
301
+ rows={[[
302
+ metric.label,
303
+ formatBandScore(studentScore),
304
+ formatBandScore(classAverage),
305
+ formatBandScore(scaleMin),
306
+ formatBandScore(scaleMax),
307
+ ]]}
308
+ />
309
+ </div>
310
+ )
311
+ }
312
+
313
+ export function DashboardQuotaProgressCard({
314
+ data = DASHBOARD_STUDENT_SCORES,
315
+ className,
316
+ }: {
317
+ data?: DashboardStudentScoresData
318
+ className?: string
319
+ }) {
320
+ const desc = data.description ?? ""
321
+
322
+ return (
323
+ <div
324
+ className={cn("grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3", className)}
325
+ role="group"
326
+ aria-label={`${data.title}. ${desc}`}
327
+ >
328
+ {data.metrics.map((m) => (
329
+ <Card
330
+ key={m.id}
331
+ className="flex flex-col overflow-visible shadow-xs"
332
+ role="figure"
333
+ aria-label={`${m.label}. ${m.description ?? desc}`}
334
+ >
335
+ <CardHeader className="shrink-0 pb-2">
336
+ <CardTitle className="text-sm font-semibold leading-tight">{m.label}</CardTitle>
337
+ <CardDescription className="text-xs">{m.description ?? desc}</CardDescription>
338
+ </CardHeader>
339
+ <CardContent className="flex flex-1 flex-col pb-4 pt-0">
340
+ <QuotaLinearProgressCardBody metric={m} suiteContext={desc} />
341
+ </CardContent>
342
+ </Card>
343
+ ))}
344
+ <Card
345
+ className="flex flex-col overflow-visible shadow-xs"
346
+ role="figure"
347
+ aria-label={`${data.radial.title}. ${desc}`}
348
+ >
349
+ <CardHeader className="shrink-0 pb-2">
350
+ <CardTitle className="text-sm font-semibold leading-tight">{data.radial.title}</CardTitle>
351
+ <CardDescription className="text-xs">{desc}</CardDescription>
352
+ </CardHeader>
353
+ <CardContent className="flex flex-1 flex-col items-center justify-center pb-4 pt-0">
354
+ <QuotaRadialGaugeStatic radial={data.radial} />
355
+ <SrOnlyMetricTable
356
+ caption={data.radial.title}
357
+ headers={["Measure", "Student score", "Class average", "Scale"]}
358
+ rows={[[
359
+ data.radial.title,
360
+ formatBandScore(data.radial.studentScore),
361
+ formatBandScore(data.radial.classAverage),
362
+ `${formatBandScore(data.radial.scaleMin)}–${formatBandScore(data.radial.scaleMax)}`,
363
+ ]]}
364
+ />
365
+ </CardContent>
366
+ </Card>
367
+ </div>
368
+ )
369
+ }
@@ -0,0 +1,69 @@
1
+ "use client"
2
+
3
+ /**
4
+ * DashboardReportCharts — single reusable layout for the main `/dashboard` “Report” tab and
5
+ * list-page **dashboard** view types (Placements, Team, etc.).
6
+ *
7
+ * Composes **`KeyMetrics`** (flat) + chart middle section + **`KeyMetrics`** (period comparison card).
8
+ * - Default **`ChartsOverview`** = placement-themed demo gallery (used by `/dashboard` and Placements).
9
+ * - Pass **`chartsSection`** for entity-specific charts (e.g. Team roster metrics) so graphs match the page.
10
+ */
11
+
12
+ import * as React from "react"
13
+ import { DashboardPromoBanner } from "@/components/dashboard-promo-banner"
14
+ import { GettingStarted } from "@/components/getting-started"
15
+ import { KeyMetrics, type MetricInsight, type MetricItem } from "@/components/key-metrics"
16
+ import { ChartsOverview, type ChartCardVariant } from "@/components/charts-overview"
17
+ import { useChartVariant } from "@/contexts/chart-variant-context"
18
+
19
+ export interface DashboardReportChartsProps {
20
+ metrics: MetricItem[]
21
+ insight: MetricInsight
22
+ /** Override app-wide chart card style (e.g. tests). */
23
+ chartVariant?: ChartCardVariant
24
+ /** When set, replaces `ChartsOverview` (placement demo). Use for Team / other hubs with their own data. */
25
+ chartsSection?: React.ReactNode
26
+ comparisonTitle?: string
27
+ comparisonDescription?: string
28
+ /** Use on dense list hubs; main dashboard Report tab omits this. */
29
+ metricsSingleRow?: boolean
30
+ }
31
+
32
+ export function DashboardReportCharts({
33
+ metrics,
34
+ insight,
35
+ chartVariant: chartVariantProp,
36
+ chartsSection,
37
+ comparisonTitle = "Period Comparison",
38
+ comparisonDescription = "Same metrics across comparison periods",
39
+ metricsSingleRow = false,
40
+ }: DashboardReportChartsProps) {
41
+ const { chartVariant: ctxVariant } = useChartVariant()
42
+ const v = (chartVariantProp ?? ctxVariant) as ChartCardVariant
43
+
44
+ return (
45
+ <div className="flex flex-col gap-4 pb-6">
46
+ <div className="px-4 lg:px-6">
47
+ <DashboardPromoBanner />
48
+ </div>
49
+ <KeyMetrics
50
+ variant="flat"
51
+ metrics={metrics}
52
+ insight={insight}
53
+ showHeader={false}
54
+ metricsSingleRow={metricsSingleRow}
55
+ />
56
+ {chartsSection ?? <ChartsOverview variant={v} />}
57
+ <GettingStarted />
58
+ <div className="px-4 lg:px-6">
59
+ <KeyMetrics
60
+ variant="card"
61
+ title={comparisonTitle}
62
+ description={comparisonDescription}
63
+ metrics={metrics}
64
+ defaultPeriod="month"
65
+ />
66
+ </div>
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,68 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Shared section chrome for dashboard hubs (Simple plain sections + Mix card headers).
5
+ */
6
+
7
+ import * as React from "react"
8
+ import { cn } from "@/lib/utils"
9
+
10
+ /** Primary line — matches across Getting started, Tasks, Insights, Learn, etc. */
11
+ export const dashboardSectionTitleClassName =
12
+ "font-sans text-base font-semibold leading-snug text-foreground"
13
+
14
+ export const dashboardSectionDescriptionClassName = "text-sm text-muted-foreground"
15
+
16
+ export function DashboardSectionTitle({
17
+ id,
18
+ as: Tag = "h2",
19
+ className,
20
+ children,
21
+ }: {
22
+ id?: string
23
+ as?: "h1" | "h2"
24
+ className?: string
25
+ children: React.ReactNode
26
+ }) {
27
+ return (
28
+ <Tag id={id} className={cn(dashboardSectionTitleClassName, className)}>
29
+ {children}
30
+ </Tag>
31
+ )
32
+ }
33
+
34
+ /** Title + optional description + optional trailing actions (e.g. Select). */
35
+ export function DashboardSectionIntro({
36
+ title,
37
+ titleAs = "h2",
38
+ titleId,
39
+ description,
40
+ actions,
41
+ className,
42
+ }: {
43
+ title: string
44
+ titleAs?: "h1" | "h2"
45
+ titleId?: string
46
+ description?: string
47
+ actions?: React.ReactNode
48
+ className?: string
49
+ }) {
50
+ return (
51
+ <div
52
+ className={cn(
53
+ "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between",
54
+ className,
55
+ )}
56
+ >
57
+ <div className="min-w-0">
58
+ <DashboardSectionTitle id={titleId} as={titleAs}>
59
+ {title}
60
+ </DashboardSectionTitle>
61
+ {description ? (
62
+ <p className={cn(dashboardSectionDescriptionClassName, "mt-0.5")}>{description}</p>
63
+ ) : null}
64
+ </div>
65
+ {actions ? <div className="shrink-0">{actions}</div> : null}
66
+ </div>
67
+ )
68
+ }